diff --git a/examples/calculator/.gitignore b/examples/calculator/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/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/calculator/.npmrc b/examples/calculator/.npmrc new file mode 100644 index 00000000..6b5f38e8 --- /dev/null +++ b/examples/calculator/.npmrc @@ -0,0 +1,2 @@ +save-exact = true +package-lock = false diff --git a/examples/calculator/README.md b/examples/calculator/README.md new file mode 100644 index 00000000..0e63872e --- /dev/null +++ b/examples/calculator/README.md @@ -0,0 +1,23 @@ +# Frontend Mentor Calculator + +Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend Mentor Calculator code challenge](https://www.frontendmentor.io/challenges/calculator-app-9lteq5N29) + +## 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/calculator/index.html b/examples/calculator/index.html new file mode 100644 index 00000000..3728b1c2 --- /dev/null +++ b/examples/calculator/index.html @@ -0,0 +1,17 @@ + + + + + + + Calculator | FrontendMentor + + +
+ + + diff --git a/examples/calculator/package.json b/examples/calculator/package.json new file mode 100644 index 00000000..c3c67fb1 --- /dev/null +++ b/examples/calculator/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontendmentor-mortgage-repayment-calculator", + "private": true, + "version": "0.85.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest" + }, + "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", + "vitest": "2.1.4" + } +} diff --git a/examples/calculator/public/assets/images/favicon-32x32.png b/examples/calculator/public/assets/images/favicon-32x32.png new file mode 100644 index 00000000..1e2df7f0 Binary files /dev/null and b/examples/calculator/public/assets/images/favicon-32x32.png differ diff --git a/examples/calculator/src/calculator.ts b/examples/calculator/src/calculator.ts new file mode 100644 index 00000000..f2c7a6e4 --- /dev/null +++ b/examples/calculator/src/calculator.ts @@ -0,0 +1,258 @@ +import { type Context } from "@grucloud/bau-ui/context"; +import themeSwitcher from "./themeSwitcher"; +import Parser, { Token } from "./parser"; +import { compute, buildRPN } from "./shuntingYard"; +import BN from "bignumber.js"; + +const locale = "US"; +const symbols = [ + ["7"], + ["8"], + ["9"], + ["DEL", "del"], + ["4"], + ["5"], + ["6"], + ["+", "add"], + ["1"], + ["2"], + ["3"], + ["-", "minus"], + [".", "dot"], + ["0"], + ["/", "divide"], + ["*", "multiply"], + ["RESET", "reset"], + ["=", "equal"], +]; + +const formatNumber = (num: string) => { + const formatted = new Intl.NumberFormat(locale, { + maximumFractionDigits: 50, + }).format(Number(num)); + return formatted.length < num.length ? num : formatted; +}; + +export default function (context: Context) { + const { bau, css, window } = context; + const { h1, article, header, section, button, div } = bau.tags; + + const tokensState = bau.state([]); + const rpn = bau.derive(() => buildRPN(tokensState.val)); + // const rpnString = bau.derive(() => + // rpn.val.map(({ value }) => value).join(" ") + // ); + const result = bau.derive(() => { + const resultValue = compute(rpn.val).resultValue; + if (resultValue) { + if (!resultValue.isNaN()) { + return formatNumber(resultValue.toString()); + } + } + }); + + const operandCurrent = bau.derive(() => + tokensState.val.reduce( + (acc, { value }) => + BN(value).isNaN() ? acc.concat(value) : acc.concat(formatNumber(value)), + "" + ) + ); + + const className = css` + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 0.3rem; + padding-inline: 1.5rem; + padding-block: 1rem; + color: var(--color-text); + min-height: 100vh; + > header { + display: flex; + justify-content: space-between; + gap: 1rem; + } + > section { + background-color: var(--secondary-background-color); + padding: 1rem; + border-radius: 0.5rem; + } + .keypad { + display: grid; + gap: 1rem; + grid-template-columns: repeat(4, 1fr); + button { + font-size: 1.8rem; + font-weight: 700; + border: none; + padding-inline: 1rem; + padding-block: 0.7rem; + cursor: pointer; + border-radius: 0.5rem; + background: var(--buttons-background-color); + color: var(--buttons-color-text); + box-shadow: inset 0px -4px 0px var(--buttons-box-shadow); + transition: all 0.1s ease-out; + &:hover { + background: var(--buttons-background-color-active); + } + &:active { + transform: translateY(1px); + box-shadow: inset 0px -1px 0px var(--buttons-box-shadow); + } + } + .key-del, + .key-reset { + background-color: var(--buttons-secondary-background-color); + box-shadow: inset 0px -4px 0px var(--buttons-secondary-box-shadow); + color: #ffffff; + font-size: 1.2rem; + &:hover { + background: var(--buttons-secondary-background-color-active); + } + } + .key-reset { + grid-column: 1 / span 2; + } + .key-equal { + grid-column: 3 / span 2; + background-color: var(--ternary-background-color); + &:hover { + background: var(--ternary-background-color-active); + } + box-shadow: inset 0px -4px 0px var(--buttons-ternary-box-shadow); + color: #ffffff; + } + } + .display { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + .operand-previous { + font-size: 1.2rem; + height: 2rem; + color: var(--color-text-secondary); + } + .operand-current { + font-size: 1.7rem; + height: 2rem; + } + } + `; + + const parser = Parser(); + + const equalSign = () => { + if (result.val != undefined) { + parser.reset(); + tokensState.val = parser.parseFormula(result.val); + } + }; + + const onclickSymbol = (symbol: any) => () => { + if (symbol[0] == "=") { + equalSign(); + } else { + tokensState.val = parser.evKey(symbol[0]); + } + }; + + const onkeydown = (event: any) => { + if (event.key == "Backspace") { + tokensState.val = parser.evKey("DEL"); + } else if (event.key == "Escape") { + parser.reset(); + tokensState.val = []; + } else if (event.key == "=") { + equalSign(); + } else { + tokensState.val = parser.evKey(event.key); + } + }; + + const ThemeSwitcher = themeSwitcher(context); + + const getKeyName = (symbol: string[]) => symbol[1] ?? symbol[0]; + + function textWidth(str: string, fontSize: Number) { + const span = document.createElement("span"); + span.style.position = "fixed"; + span.style.visibility = "hidden"; + span.style.fontSize = `${fontSize}px`; + span.innerText = str; + document.body.appendChild(span); + const width = Math.round(span.getBoundingClientRect().width); + span.remove(); + return width; + } + + const doFontSize = (state: any, fsStart = 20) => { + let fs = fsStart; + if (!state.val) { + return fs; + } + const el = document.getElementsByClassName("display")[0]; + if (!el) { + return fs; + } + const width = el.getBoundingClientRect().width; + while (fs > 8) { + if (textWidth(state.val, fs) + 50 > width) { + fs--; + } else { + break; + } + } + return fs; + }; + + const fsOperandCurrent = bau.derive(() => doFontSize(operandCurrent, 22)); + const fsResult = bau.derive(() => doFontSize(result, 18)); + + return () => { + return article( + { + class: className, + bauMounted: () => { + window.document.body.addEventListener("keydown", onkeydown); + }, + bauUnmounted: () => { + window.document.removeEventListener("keydown", onkeydown); + }, + }, + header(h1("Calc"), ThemeSwitcher()), + section( + { class: "display" }, + div( + { + style: () => `font-size: ${fsResult.val}px`, + class: "operand-previous", + }, + result + ), + div( + { + style: () => `font-size: ${fsOperandCurrent.val}px`, + class: "operand-current", + }, + operandCurrent + ) + ), + section( + { class: "keypad" }, + symbols.map((symbol) => + button( + { + type: "button", + class: `key-${getKeyName(symbol)}`, + onclick: onclickSymbol(symbol), + }, + symbol[0] + ) + ) + ) + ); + }; +} diff --git a/examples/calculator/src/main.ts b/examples/calculator/src/main.ts new file mode 100644 index 00000000..f8006eb7 --- /dev/null +++ b/examples/calculator/src/main.ts @@ -0,0 +1,20 @@ +import { createContext, type Context } from "@grucloud/bau-ui/context"; +import calculator from "./calculator"; + +import "./style.css"; + +const context = createContext(); + +const app = (context: Context) => { + const { bau } = context; + const { main } = bau.tags; + + const Calculator = calculator(context); + + return function () { + return main(Calculator()); + }; +}; + +const App = app(context); +document.getElementById("app")?.replaceChildren(App()); diff --git a/examples/calculator/src/parser.ts b/examples/calculator/src/parser.ts new file mode 100644 index 00000000..c35738c5 --- /dev/null +++ b/examples/calculator/src/parser.ts @@ -0,0 +1,126 @@ +export const TOKEN_TYPE = { NUMERIC: "NUMERIC", OPERATOR: "OPERATOR" }; + +const STATES = { + INIT: "INIT", + COLLECT_DIGIT: "COLLECT_DIGIT", + COLLECT_OPERATOR: "COLLECT_OPERATOR", +}; + +export type Token = { + value: string; + type: string; +}; + +const isDigit = (key: string) => { + const nkey = Number(key); + return nkey >= 0 && nkey <= 9 ? true : false; +}; + +const isDot = (key: string) => key == "."; +const isMinus = (key: string) => key == "-"; + +const isOperator = (key: string) => ["*", "/", "+", "-", "="].includes(key); +const isDelete = (key: string) => key == "DEL"; +const isReset = (key: string) => key == "RESET"; + +const Parser = () => { + let _formula = ""; + const parseFormula = (formula: string) => { + _formula = formula; + let stateCurrent = STATES.INIT; + let stateNext = STATES.INIT; + const tokens: Token[] = []; + let tokenCurrent: string = ""; + + const onTokenNew = (token: Token) => tokens.unshift(token); + + const onTokenUpdate = (token: Token) => { + if (tokens.length > 0) { + tokens[0] = token; + } + }; + + const tokenUpdate = (key: string) => { + tokenCurrent = tokenCurrent.concat(key); + }; + + const resetToken = () => { + tokenCurrent = ""; + }; + + formula.split("").forEach((key) => { + stateNext = ""; + switch (stateCurrent) { + case STATES.INIT: { + if (isDigit(key) || isDot(key) || isMinus(key)) { + stateNext = STATES.COLLECT_DIGIT; + } else if (isOperator(key)) { + stateNext = STATES.COLLECT_OPERATOR; + } + break; + } + case STATES.COLLECT_DIGIT: { + if (isDigit(key) || (isDot(key) && !tokenCurrent.includes("."))) { + tokenUpdate(key); + onTokenUpdate({ value: tokenCurrent, type: TOKEN_TYPE.NUMERIC }); + } else if (isOperator(key)) { + stateNext = STATES.COLLECT_OPERATOR; + } + break; + } + case STATES.COLLECT_OPERATOR: { + if (isDigit(key) || isDot(key) || isMinus(key)) { + stateNext = STATES.COLLECT_DIGIT; + } + //Ignore isOperator + break; + } + default: { + throw Error("Invalid State"); + } + } + // On Entry + switch (stateNext) { + case STATES.INIT: { + resetToken(); + break; + } + case STATES.COLLECT_DIGIT: { + tokenUpdate(key); + onTokenNew({ value: tokenCurrent, type: TOKEN_TYPE.NUMERIC }); + break; + } + case STATES.COLLECT_OPERATOR: { + resetToken(); + onTokenNew({ value: key, type: TOKEN_TYPE.OPERATOR }); + break; + } + } + if (stateNext) { + stateCurrent = stateNext; + } + }); + return tokens.reverse(); + }; + + const reset = () => { + _formula = ""; + }; + + return { + reset, + parseFormula, + evKey: (key: string) => { + if (isReset(key)) { + reset(); + } else if (isDelete(key)) { + _formula = _formula.slice(0, -1); + } else if (isDigit(key) || isDot(key) || isOperator(key)) { + _formula = _formula.concat(key); + } + return parseFormula(_formula); + }, + }; +}; + +export default Parser; diff --git a/examples/calculator/src/shuntingYard.ts b/examples/calculator/src/shuntingYard.ts new file mode 100644 index 00000000..71fbe0f0 --- /dev/null +++ b/examples/calculator/src/shuntingYard.ts @@ -0,0 +1,93 @@ +import { TOKEN_TYPE, Token } from "./parser"; +import BN from "bignumber.js"; + +const OperatorMap: Record = { + "/": 4, + "*": 3, + "+": 2, + "-": 2, +}; + +const getPrecedence = (token: Token) => OperatorMap[token.value]; + +// Returns the Reversed Polished Notation +export const buildRPN = (tokens: Token[]) => { + const output: Token[] = []; + const stack: Token[] = []; + + tokens.forEach((token) => { + if (token.type == TOKEN_TYPE.NUMERIC) { + output.push(token); + } else if (token.type == TOKEN_TYPE.OPERATOR) { + while (stack.length > 0) { + const itToken = stack[0]; + if (getPrecedence(itToken) >= getPrecedence(token)) { + stack.shift(); + output.push(itToken); + } else { + break; + } + } + stack.unshift(token); + } + }); + while (stack.length > 0) { + const toMove = stack.shift(); + if (toMove) { + output.push(toMove); + } + } + + return output; +}; + +export const compute = (tokens: Token[]) => { + const results: BN[] = []; + let error; + tokens.every((token) => { + if (token.type == TOKEN_TYPE.NUMERIC) { + results.unshift(BN(token.value)); + } else if (token.type == TOKEN_TYPE.OPERATOR) { + const op = token.value; + // Assume operators consumes 2 operands + const operand2 = results.shift(); + if (!operand2) { + error = "missing operand"; + return false; + } + const operand1 = results.shift(); + if (!operand1) { + error = "missing operand"; + return false; + } + let result; + switch (op) { + case "*": { + result = operand1.times(operand2); + break; + } + case "/": { + result = operand1.dividedBy(operand2); + break; + } + case "+": { + result = operand1.plus(operand2); + break; + } + case "-": { + result = operand1.minus(operand2); + break; + } + } + if (result) { + results.unshift(result); + } + } + return true; + }); + let resultValue; + if (results.length == 1) { + resultValue = results[0]; + } + return { results, error, resultValue }; +}; diff --git a/examples/calculator/src/style.css b/examples/calculator/src/style.css new file mode 100644 index 00000000..bacc52d3 --- /dev/null +++ b/examples/calculator/src/style.css @@ -0,0 +1,72 @@ +@import url("https://fonts.googleapis.com/css2?family=Spartan:wght@700&display=swap"); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-background-color: #3a4663; + --color-text: #fff; + --color-text-secondary: #9b9b9b; + + --secondary-background-color: #242d44; + --ternary-background-color: #d03f2f; + --ternary-background-color-active: #f96b5b; + --display-background-color: #181f33; + + --buttons-background-color: #eae3dc; + --buttons-background-color-active: #fffffe; + --buttons-secondary-background-color: #647198; + --buttons-secondary-background-color-active: #a2b2e1; + --buttons-secondary-box-shadow: #414e73; + --buttons-ternary-box-shadow: #93261a; + --buttons-color-text: #434a59; + --buttons-box-shadow: #b3a497; +} + +[data-theme="second"] { + --primary-background-color: #e6e6e6; + --color-text: #36362c; + --secondary-background-color: #d2cdcd; + --ternary-background-color: #c85402; + --ternary-background-color-active: #ff8a38; + --display-background-color: #eeeeee; + --buttons-background-color: #e5e4e1; + --buttons-background-color-active: #ffffff; + --buttons-secondary-background-color: #378187; + --buttons-secondary-background-color-active: #62b5bc; + --buttons-secondary-box-shadow: #1b6066; + --buttons-ternary-box-shadow: #873901; + --buttons-color-text: var(--color-texts); + --buttons-box-shadow: #a79e91; +} + +[data-theme="third"] { + --primary-background-color: #17062a; + --color-text: #ffe53d; + --secondary-background-color: #1e0936; + --ternary-background-color: #00ded0; + --ternary-background-color-active: #93fff8; + --display-background-color: #1e0936; + --buttons-background-color: #331c4d; + --buttons-background-color-active: #6c34ac; + --buttons-secondary-background-color: #56077c; + --buttons-secondary-background-color: #8631af; + --buttons-secondary-box-shadow: #be15f4; + --buttons-ternary-box-shadow: #6cf9f1; + --buttons-color-text: var(--color-texts); + --buttons-box-shadow: #881c9e; +} + +body { + background-color: var(--primary-background-color); + font-family: "Spartan", sans-serif; + min-height: 100vh; + display: grid; + place-items: center; + @media (max-width: 500px) { + place-items: flex-start; + } +} diff --git a/examples/calculator/src/themeSwitcher.ts b/examples/calculator/src/themeSwitcher.ts new file mode 100644 index 00000000..297d454f --- /dev/null +++ b/examples/calculator/src/themeSwitcher.ts @@ -0,0 +1,102 @@ +import { type Context } from "@grucloud/bau-ui/context"; + +const themes = ["first", "second", "third"]; + +export default function (context: Context) { + const { bau, css, window } = context; + const { section, span, input, div, label } = bau.tags; + + const className = css` + display: flex; + align-items: flex-end; + gap: 0.4rem; + + > span { + font-size: 0.6rem; + text-transform: uppercase; + letter-spacing: 0.1rem; + } + + label { + font-size: 0.6rem; + cursor: pointer; + } + + input { + cursor: pointer; + appearance: none; + width: 1.2rem; + height: 1.2rem; + } + + .label-container { + display: flex; + justify-content: space-around; + } + .input-container { + display: flex; + position: relative; + margin-inline: 0.2rem; + background: var(--secondary-background-color); + border-radius: 1rem; + &::before { + position: absolute; + content: ""; + left: 5%; + top: 50%; + transform: translateY(-50%); + height: 0.6rem; + width: 0.6rem; + border-radius: 50%; + background-color: var(--ternary-background-color); + transition: all 0.5s; + } + &:has(input[id="second"]:checked) { + &::before { + left: 50%; + transform: translate(-50%, -50%); + } + } + &:has(input[id="third"]:checked) { + &::before { + left: 95%; + transform: translate(-100%, -50%); + } + } + } + `; + + const onsubmitTheme = (event: any) => { + window.document.documentElement.setAttribute("data-theme", event.target.id); + }; + + return () => + section( + { + class: className, + onclick: onsubmitTheme, + }, + span("Theme"), + div( + div( + { + class: "label-container", + }, + themes.map((id, index) => label({ htmlFor: id }, index + 1)) + ), + div( + { + class: "input-container", + }, + themes.map((id) => + input({ + type: "radio", + name: "themeRadio", + id, + oninput: onsubmitTheme, + }) + ) + ) + ) + ); +} diff --git a/examples/calculator/src/vite-env.d.ts b/examples/calculator/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/calculator/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/calculator/test/parser.test.ts b/examples/calculator/test/parser.test.ts new file mode 100644 index 00000000..57eb64dd --- /dev/null +++ b/examples/calculator/test/parser.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, it, assert } from "vitest"; + +import Parser, { TOKEN_TYPE } from "../src/parser"; + +describe("parser", async () => { + const parser = Parser(); + + beforeEach(() => { + parser.reset(); + }); + + it("1+1=", () => { + "1+1=".split("").forEach((key) => parser.evKey(key)); + const tokens = parser.evKey("1"); + assert(tokens); + }); + it("-1+1", () => { + const tokens = parser.parseFormula("-1+1"); + console.log(JSON.stringify(tokens)); + assert.equal(tokens.length, 3); + assert.equal(tokens[0].value, "-1"); + assert.equal(tokens[0].type, TOKEN_TYPE.NUMERIC); + assert.equal(tokens[1].type, TOKEN_TYPE.OPERATOR); + }); + it("1+2*3", () => { + const tokens = parser.parseFormula("1+2*3"); + assert.equal(tokens.length, 5); + }); + it("1.1+0.0", () => { + const tokens = parser.parseFormula("1.1+0.0"); + assert.equal(tokens.length, 3); + assert.equal(tokens[0].value, "1.1"); + assert.equal(tokens[0].type, TOKEN_TYPE.NUMERIC); + assert.equal(tokens[1].type, TOKEN_TYPE.OPERATOR); + }); + it("1", () => { + const tokens = parser.parseFormula("1"); + assert.equal(tokens.length, 1); + assert.equal(tokens[0].value, "1"); + }); + it("/", () => { + const tokens = parser.parseFormula("/"); + assert.equal(tokens.length, 1); + assert.equal(tokens[0].value, "/"); + }); +}); diff --git a/examples/calculator/test/shuntingYard.test.ts b/examples/calculator/test/shuntingYard.test.ts new file mode 100644 index 00000000..d3f8e275 --- /dev/null +++ b/examples/calculator/test/shuntingYard.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, it, assert } from "vitest"; + +import Parser from "../src/parser"; +import { buildRPN, compute } from "../src/shuntingYard"; + +const rpnToString = (tokens) => tokens.map(({ value }) => value).join(" "); + +describe("shunting yard", async () => { + const parser = Parser(); + + beforeEach(() => { + parser.reset(); + }); + + it("1+2", () => { + const rpn = buildRPN(parser.parseFormula("1+2")); + assert.equal(rpnToString(rpn), "1 2 +"); + assert.equal(compute(rpn).resultValue.toString(), 3); + }); + it("-1-2-3", () => { + const rpn = buildRPN(parser.parseFormula("-1-2-3")); + assert.equal(rpnToString(rpn), "-1 2 - 3 -"); + assert.equal(compute(rpn).resultValue.toString(), "-6"); + }); + it("2*-1", () => { + const rpn = buildRPN(parser.parseFormula("2*-1")); + assert.equal(rpnToString(rpn), "2 -1 *"); + assert.equal(compute(rpn).resultValue.toString(), "-2"); + }); + it("1+2+3", () => { + const rpn = buildRPN(parser.parseFormula("1+2+3")); + assert.equal(rpnToString(rpn), "1 2 + 3 +"); + assert.equal(compute(rpn).resultValue.toString(), "6"); + }); + it("2-1+3", () => { + const rpn = buildRPN(parser.parseFormula("2-1+3")); + assert.equal(rpnToString(rpn), "2 1 - 3 +"); + assert.equal(compute(rpn).resultValue.toString(), 4); + }); + it("1+2*4", () => { + const rpn = buildRPN(parser.parseFormula("1+2*4")); + assert.equal(rpnToString(rpn), "1 2 4 * +"); + assert.equal(compute(rpn).resultValue.toString(), "9"); + }); + it("4/2", () => { + const rpn = buildRPN(parser.parseFormula("4/2")); + assert.equal(rpnToString(rpn), "4 2 /"); + assert.equal(compute(rpn).resultValue.toString(), "2"); + }); + it("4/2*2", () => { + const rpn = buildRPN(parser.parseFormula("4/2*2")); + assert.equal(rpnToString(rpn), "4 2 / 2 *"); + assert.equal(compute(rpn).resultValue.toString(), "4"); + }); + it("minus", () => { + const rpn = buildRPN(parser.parseFormula("-")); + assert.equal(rpnToString(rpn), "-"); + assert.equal(compute(rpn).resultValue.toString(), "NaN"); + }); + it("1*-.2", () => { + const rpn = buildRPN(parser.parseFormula("1*-.2")); + assert.equal(rpnToString(rpn), "1 -.2 *"); + assert.equal(compute(rpn).resultValue.toString(), "-0.2"); + }); +}); diff --git a/examples/calculator/tsconfig.json b/examples/calculator/tsconfig.json new file mode 100644 index 00000000..75abdef2 --- /dev/null +++ b/examples/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/calculator/vite.config.js b/examples/calculator/vite.config.js new file mode 100644 index 00000000..41713bec --- /dev/null +++ b/examples/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/contact-form/package.json b/examples/contact-form/package.json index c17282b6..069be014 100644 --- a/examples/contact-form/package.json +++ b/examples/contact-form/package.json @@ -1,5 +1,5 @@ { - "name": "frontendmentor-mortgage-repayment-calculator", + "name": "frontendmentor-contact-form", "private": true, "version": "0.85.0", "type": "module",