diff --git a/examples/mortgage-repayment-calculator/.gitignore b/examples/mortgage-repayment-calculator/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/mortgage-repayment-calculator/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/mortgage-repayment-calculator/.npmrc b/examples/mortgage-repayment-calculator/.npmrc new file mode 100644 index 00000000..6b5f38e8 --- /dev/null +++ b/examples/mortgage-repayment-calculator/.npmrc @@ -0,0 +1,2 @@ +save-exact = true +package-lock = false diff --git a/examples/mortgage-repayment-calculator/README.md b/examples/mortgage-repayment-calculator/README.md new file mode 100644 index 00000000..157ed120 --- /dev/null +++ b/examples/mortgage-repayment-calculator/README.md @@ -0,0 +1,23 @@ +# Frontend Mentor Mortgage Repayment Calculator + +Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend Mentor Mortgage Repayment Calculator code challenge](https://www.frontendmentor.io/challenges/mortgage-repayment-calculator-Galx1LXK73) + +## Workflow + +Install the dependencies: + +```sh +npm install +``` + +Start a development server: + +```sh +npm run dev +``` + +Build a production version: + +```sh +npm run build +``` diff --git a/examples/mortgage-repayment-calculator/index.html b/examples/mortgage-repayment-calculator/index.html new file mode 100644 index 00000000..a5c174a1 --- /dev/null +++ b/examples/mortgage-repayment-calculator/index.html @@ -0,0 +1,17 @@ + + + + + + + Mortgage Repayment Calculator | FrontendMentor + + +
+ + + diff --git a/examples/mortgage-repayment-calculator/package.json b/examples/mortgage-repayment-calculator/package.json new file mode 100644 index 00000000..c17282b6 --- /dev/null +++ b/examples/mortgage-repayment-calculator/package.json @@ -0,0 +1,21 @@ +{ + "name": "frontendmentor-mortgage-repayment-calculator", + "private": true, + "version": "0.85.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.0.2", + "vite": "^5.2.11" + }, + "dependencies": { + "@grucloud/bau": "^0.85.0", + "@grucloud/bau-css": "^0.85.0", + "@grucloud/bau-ui": "^0.85.0", + "bignumber.js": "9.1.2" + } +} diff --git a/examples/mortgage-repayment-calculator/public/assets/images/favicon-32x32.png b/examples/mortgage-repayment-calculator/public/assets/images/favicon-32x32.png new file mode 100644 index 00000000..1e2df7f0 Binary files /dev/null and b/examples/mortgage-repayment-calculator/public/assets/images/favicon-32x32.png differ diff --git a/examples/mortgage-repayment-calculator/public/assets/images/icon-calculator.svg b/examples/mortgage-repayment-calculator/public/assets/images/icon-calculator.svg new file mode 100644 index 00000000..4510a5ce --- /dev/null +++ b/examples/mortgage-repayment-calculator/public/assets/images/icon-calculator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/mortgage-repayment-calculator/public/assets/images/illustration-empty.svg b/examples/mortgage-repayment-calculator/public/assets/images/illustration-empty.svg new file mode 100644 index 00000000..8f164f26 --- /dev/null +++ b/examples/mortgage-repayment-calculator/public/assets/images/illustration-empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/mortgage-repayment-calculator/src/main.ts b/examples/mortgage-repayment-calculator/src/main.ts new file mode 100644 index 00000000..94bc7da7 --- /dev/null +++ b/examples/mortgage-repayment-calculator/src/main.ts @@ -0,0 +1,20 @@ +import { createContext, type Context } from "@grucloud/bau-ui/context"; +import mortgageRepaymentCalculator from "./mortgageRepaymentCalculator"; + +import "./style.css"; + +const context = createContext(); + +const app = (context: Context) => { + const { bau } = context; + const { main } = bau.tags; + + const MortgageRepaymentCalculator = mortgageRepaymentCalculator(context); + + return function () { + return main(MortgageRepaymentCalculator()); + }; +}; + +const App = app(context); +document.getElementById("app")?.replaceChildren(App()); diff --git a/examples/mortgage-repayment-calculator/src/mortgageRepaymentCalculator.ts b/examples/mortgage-repayment-calculator/src/mortgageRepaymentCalculator.ts new file mode 100644 index 00000000..14a5004e --- /dev/null +++ b/examples/mortgage-repayment-calculator/src/mortgageRepaymentCalculator.ts @@ -0,0 +1,295 @@ +import { type Context } from "@grucloud/bau-ui/context"; + +import BN from "bignumber.js"; + +const locale = "en-GB"; +const currency = "GBP"; + +const formatCurrency = (number: number) => + new Intl.NumberFormat(locale, { style: "currency", currency }).format(number); + +export default function (context: Context) { + const { bau, css } = context; + const { + h1, + form, + p, + article, + section, + header, + span, + label, + input, + div, + button, + hr, + img, + } = bau.tags; + + const monthlyRepaymentState = bau.state(""); + const totalState = bau.state(""); + const className = css` + display: grid; + grid-template-columns: repeat(2, minmax(0, 400px)); + border-radius: 1rem; + overflow: hidden; + margin-inline: 0.5rem; + @media (max-width: 600px) { + grid-template-columns: 1fr; + } + > section { + } + + .calculator-form { + padding: 1rem; + background-color: var(--white); + header { + display: flex; + justify-content: space-between; + button { + text-decoration: underline; + background: none; + color: var(--grey-700); + } + } + form { + button[type="submit"] { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + &::before { + content: url("./assets/images/icon-calculator.svg"); + } + border-radius: 2rem; + } + } + } + .result-container { + background-color: var(--white); + } + .result { + padding: 1rem; + + color: var(--grey-100); + background-color: var(--grey-900); + border-bottom-left-radius: 4rem; + display: flex; + height: 100%; + flex-direction: column; + gap: 1rem; + &.no-result { + align-items: center; + > img { + width: 192px; + height: 192px; + } + } + + p, + span { + color: var(--grey-300); + } + .payments { + background-color: var(--grey-1000); + display: flex; + flex-direction: column; + padding: 1rem; + border-radius: 0.5rem; + border-top: 3px solid var(--primary); + hr { + border: 1px solid var(--grey-900); + } + .monthly-payments-value { + color: var(--primary); + font-size: 2.6rem; + font-weight: 700; + line-height: 3.5rem; + } + .total-payments-value { + font-size: 2rem; + line-height: 2.5rem; + color: var(--grey-100); + } + } + } + `; + + const clearAll = (event: any) => { + event.target.closest("form").reset(); + monthlyRepaymentState.val = ""; + totalState.val; + }; + + const onsubmit = (event: any) => { + event.preventDefault(); + const result = Object.fromEntries(new FormData(event.currentTarget)); + const { amount, term, rate, mortgateType } = result; + if (mortgateType == "repayment") { + const month = BN(term.toString()).times(12); + + const ratePerMonth = BN(1) + .plus(BN(rate.toString()).dividedBy(100).dividedBy(12)) + .pow(month); + + const newMonthlyRepayment = BN(amount.toString()) + .times(BN(rate.toString()).dividedBy(100).dividedBy(12)) + .times(ratePerMonth) + .dividedBy(ratePerMonth.minus(1)); + monthlyRepaymentState.val = formatCurrency( + newMonthlyRepayment.toNumber() + ); + totalState.val = formatCurrency( + newMonthlyRepayment.times(12).times(term.toString()).toNumber() + ); + } else if (mortgateType == "interestOnly") { + const newMonthlyRepayment = BN(amount.toString()).times( + BN(rate.toString()).dividedBy(100).dividedBy(12) + ); + monthlyRepaymentState.val = formatCurrency( + newMonthlyRepayment.toNumber() + ); + totalState.val = formatCurrency( + newMonthlyRepayment.times(term.toString()).times(12).toNumber() + ); + } + }; + + return function MortgageRepaymentCalculator() { + return article( + { class: className }, + section( + { class: "calculator-form" }, + form( + { onsubmit }, + header( + h1("Mortgage Calculator"), + button({ type: "button", onclick: clearAll }, "Clear all") + ), + label( + span("Mortgage Amount"), + div( + { + class: "input-unit", + }, + span("£"), + input({ + autofocus: true, + name: "amount", + type: "number", + required: true, + }) + ) + ), + div( + { + class: css` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + `, + }, + label( + "Mortgage Terms", + div( + { + class: "input-unit", + }, + + input({ + name: "term", + type: "number", + required: true, + min: 1, + //defaultValue: 25, + }), + span("years") + ) + ), + label( + "Interest Rate", + div( + { + class: "input-unit", + }, + input({ + name: "rate", + type: "number", + required: true, + step: 0.1, + min: 0.1, + //defaultValue: 5, + }), + span("%") + ) + ) + ), + label( + "Mortgage Type", + div( + { + class: css` + > label { + display: flex; + gap: 0.5rem; + } + `, + }, + label( + input({ + type: "radio", + name: "mortgateType", + value: "repayment", + required: true, + }), + "Repayment" + ), + label( + input({ + type: "radio", + name: "mortgateType", + value: "interestOnly", + }), + "Interest Only" + ) + ) + ), + button({ type: "submit" }, "Calculate Repayment") + ) + ), + section({ class: "result-container" }, () => + monthlyRepaymentState.val == "" + ? section( + { class: "result no-result" }, + img({ src: "./assets/images/illustration-empty.svg", alt: "" }), + h1("Results shown here"), + p( + "Complete the form and click “calculate repayments” to see what your monthly repayments would be." + ) + ) + : section( + { class: "result ok" }, + h1("Your results"), + p( + "Your results are shown below based on the information you provided. To adjust the results, edit the form and click “calculate repayments” again." + ), + div( + { class: "payments" }, + div( + { class: "monthly-payments" }, + p("Your monthly repayments"), + p({ class: "monthly-payments-value" }, monthlyRepaymentState) + ), + hr, + div( + { class: "total-payments" }, + p("Total you'll repay over the term"), + span({ class: "total-payments-value" }, totalState) + ) + ) + ) + ) + ); + }; +} diff --git a/examples/mortgage-repayment-calculator/src/style.css b/examples/mortgage-repayment-calculator/src/style.css new file mode 100644 index 00000000..66599dd6 --- /dev/null +++ b/examples/mortgage-repayment-calculator/src/style.css @@ -0,0 +1,120 @@ +/**@import url("https://fonts.googleapis.com/css2?family=Fraunces:wght@700&family=Montserrat:wght@500;700&display=swap");**/ + +* { + margin: 0; + box-sizing: border-box; +} + +:root { + /** Primary **/ + --primary: hsl(61, 70%, 52%); + --primary-200: hsl(61, 70%, 92%); + + --danger: hsl(4, 69%, 50%); + + /** Neutral **/ + + --white: hsl(0, 0%, 100%); + --grey-100: hsl(202, 86%, 94%); + --grey-300: hsl(203, 41%, 72%); + --grey-500: hsl(200, 26%, 54%); + --grey-700: hsl(200, 24%, 40%); + --grey-900: hsl(202, 55%, 16%); + --grey-1000: hsl(202, 55%, 8%); +} + +button { + border: none; + cursor: pointer; + padding: 0.7rem; + font-weight: 600; +} + +button[type="submit"] { + font-weight: 600; + font-size: 1rem; + border-radius: 0.5rem; + color: var(--grey-900); + background-color: var(--primary); +} + +label { + color: var(--grey-700); + font-size: 0.8rem; + font-weight: 600; + margin-bottom: 1rem; + line-height: 2rem; +} + +label:has(> input[type="radio"]) { + cursor: pointer; + border-radius: 0.5rem; + padding: 0.5rem; + border: 1px solid var(--grey-500); + font-weight: 600; + font-size: 0.9rem; + &:hover { + border-color: var(--grey-900); + } +} + +label:has(> input[type="radio"]:checked) { + background-color: var(--primary-200); +} + +input[type="radio"] { + accent-color: var(--primary); +} + +input[type="number"] { + width: 100%; + padding: 0.4rem; + font-size: 1rem; + font-weight: 600; + color: var(--grey-700); + border-radius: 0.5rem; + border: 1px solid var(--grey-500); +} + +.input-unit { + display: flex; + outline: 1px solid var(--grey-500); + border-radius: 0.3rem; + overflow: hidden; + &:focus-within { + outline: 2px solid var(--primary); + > span { + background-color: var(--primary); + } + } + &:has(input[type="number"]:user-invalid) { + outline: 2px solid var(--danger); + span { + color: white; + background-color: var(--danger); + } + } + + > span { + padding-inline: 0.8rem; + text-align: center; + background-color: var(--grey-100); + } + > input { + border: transparent; + &:focus { + outline: none; + } + } +} + +body { + background-color: var(--grey-100); + font-family: "Montserrat", sans-serif; + min-height: 100vh; + display: grid; + place-items: center; + @media (max-width: 600px) { + place-items: flex-start; + } +} diff --git a/examples/mortgage-repayment-calculator/src/vite-env.d.ts b/examples/mortgage-repayment-calculator/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/mortgage-repayment-calculator/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/mortgage-repayment-calculator/tsconfig.json b/examples/mortgage-repayment-calculator/tsconfig.json new file mode 100644 index 00000000..75abdef2 --- /dev/null +++ b/examples/mortgage-repayment-calculator/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/mortgage-repayment-calculator/vite.config.js b/examples/mortgage-repayment-calculator/vite.config.js new file mode 100644 index 00000000..41713bec --- /dev/null +++ b/examples/mortgage-repayment-calculator/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; + +export default defineConfig(({ command, mode, ssrBuild }) => { + return { + server: { + open: true, + }, + }; +}); diff --git a/examples/product-list-cart/src/productListCart.ts b/examples/product-list-cart/src/productListCart.ts index 9c9bca13..d285d699 100644 --- a/examples/product-list-cart/src/productListCart.ts +++ b/examples/product-list-cart/src/productListCart.ts @@ -4,7 +4,7 @@ import BN from "bignumber.js"; import data from "./data.json"; const locale = "en-US"; -const currency = "EUR"; +const currency = "USD"; const formatCurrency = (number: number) => new Intl.NumberFormat(locale, { style: "currency", currency }).format(number);