Skip to content

Commit

Permalink
calculator
Browse files Browse the repository at this point in the history
  • Loading branch information
FredericHeem committed Nov 8, 2024
1 parent 0fc5489 commit c0267b0
Show file tree
Hide file tree
Showing 18 changed files with 905 additions and 1 deletion.
24 changes: 24 additions & 0 deletions examples/calculator/.gitignore
Original file line number Diff line number Diff line change
@@ -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?
2 changes: 2 additions & 0 deletions examples/calculator/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
save-exact = true
package-lock = false
23 changes: 23 additions & 0 deletions examples/calculator/README.md
Original file line number Diff line number Diff line change
@@ -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
```
17 changes: 17 additions & 0 deletions examples/calculator/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/png"
href="./assets/images/favicon-32x32.png"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Calculator | FrontendMentor</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions examples/calculator/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
258 changes: 258 additions & 0 deletions examples/calculator/src/calculator.ts
Original file line number Diff line number Diff line change
@@ -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<Token[]>([]);
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]
)
)
)
);
};
}
20 changes: 20 additions & 0 deletions examples/calculator/src/main.ts
Original file line number Diff line number Diff line change
@@ -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());
Loading

0 comments on commit c0267b0

Please sign in to comment.