aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/js-small-apps/calculator
diff options
context:
space:
mode:
Diffstat (limited to 'public/projects/js-small-apps/calculator')
-rw-r--r--public/projects/js-small-apps/calculator/README.md13
-rw-r--r--public/projects/js-small-apps/calculator/app.js265
-rw-r--r--public/projects/js-small-apps/calculator/index.html103
-rw-r--r--public/projects/js-small-apps/calculator/style.css147
4 files changed, 528 insertions, 0 deletions
diff --git a/public/projects/js-small-apps/calculator/README.md b/public/projects/js-small-apps/calculator/README.md
new file mode 100644
index 0000000..ed8883b
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/README.md
@@ -0,0 +1,13 @@
+# Calculator
+
+A calculator implementation using HTML, CSS and JS.
+
+You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Calculator-App.md
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#calculator
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/calculator/app.js b/public/projects/js-small-apps/calculator/app.js
new file mode 100644
index 0000000..683a1e7
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/app.js
@@ -0,0 +1,265 @@
+const appHistory = ["0"];
+
+/**
+ * Check if a value is numeric.
+ * @param {String} value - A value to test.
+ * @returns {Boolean} True if value is numeric; false otherwise.
+ */
+function isNumeric(value) {
+ return !isNaN(value);
+}
+
+/**
+ * Check if a value is an operation (+, -, *, /, =).
+ * @param {String} value - A value to test.
+ * @returns {Boolean} True if value is an operation; false otherwise.
+ */
+function isOperation(value) {
+ return "+-*/=".includes(value);
+}
+
+/**
+ * Check if the value exceeds the limit of 8 characters.
+ * @param {String} value - The value to test.
+ * @returns True if the length is greater than 8; false otherwise.
+ */
+function isDigitsLimitReached(value) {
+ const digitsPart = value.split(".")[0];
+ return digitsPart?.length > 8 ? true : false;
+}
+
+/**
+ * Check if the decimal part exceeds the limit of 3 characters.
+ * @param {String} value - The value to test.
+ * @returns True if the decimal part is greater than 3; false otherwise.
+ */
+function isDecimalLimitReached(value) {
+ const decimalPart = value.split(".")[1];
+ return decimalPart?.length > 3 ? true : false;
+}
+
+/**
+ * Retrieve the last history value.
+ * @returns {String} The last history input.
+ */
+function getLastHistoryInput() {
+ return appHistory.slice(-1)[0];
+}
+
+/**
+ * Update the calculator display.
+ * @param {String} value - The value to print.
+ */
+function updateDisplay(value) {
+ const display = document.querySelector(".calculator__display");
+
+ if (isDigitsLimitReached(value) || isDecimalLimitReached(value)) {
+ display.textContent = "ERR";
+ } else {
+ display.textContent = value;
+ }
+}
+
+/**
+ * Calculate the result of an operation.
+ * @param {Number} number1 - The left number of operation.
+ * @param {Number} number2 - The right number of operation.
+ * @param {String} operation - An operation (+, -, *, /).
+ * @returns {Number} The operation result.
+ */
+function calculate(number1, number2, operation) {
+ let result;
+
+ switch (operation) {
+ case "+":
+ result = number1 + number2;
+ break;
+ case "-":
+ result = number1 - number2;
+ break;
+ case "*":
+ result = number1 * number2;
+ break;
+ case "/":
+ result = number1 / number2;
+ default:
+ break;
+ }
+
+ return result;
+}
+
+/**
+ * Get the result of an operation.
+ * @returns {Number} The operation result.
+ */
+function getResult() {
+ const historyCopy = appHistory.slice(0);
+ const number2 = Number(historyCopy.pop());
+ const operation = historyCopy.pop();
+ const number1 = Number(historyCopy.pop());
+ const result = calculate(number1, number2, operation);
+
+ return result;
+}
+
+/**
+ * Handle digit input.
+ * @param {String} value - The digit value.
+ */
+function handleDigits(value) {
+ const lastInput = getLastHistoryInput();
+ const beforeLastInput = appHistory.slice(-2)[0];
+ let newInput;
+
+ if (isNaN(lastInput) || beforeLastInput === "=") {
+ newInput = value;
+ } else {
+ appHistory.pop();
+ newInput = lastInput === "0" ? value : `${lastInput}${value}`;
+ }
+
+ if (isDigitsLimitReached(newInput) || isDecimalLimitReached(newInput)) {
+ newInput = newInput.slice(0, -1);
+ }
+
+ appHistory.push(newInput);
+ updateDisplay(newInput);
+}
+
+/**
+ * Handle operation input.
+ * @param {String} value - The operation.
+ * @returns {void}
+ */
+function handleOperation(value) {
+ const lastInput = getLastHistoryInput();
+
+ if (isOperation(lastInput)) return;
+
+ const result = getResult();
+
+ if (result) {
+ appHistory.push("=");
+ appHistory.push(`${result}`);
+ updateDisplay(`${result}`);
+ }
+
+ if (value !== "=") appHistory.push(value);
+}
+
+/**
+ * Handle number sign.
+ * @returns {void}
+ */
+function handleNumberSign() {
+ const lastInput = getLastHistoryInput();
+ if (isNaN(lastInput)) return;
+
+ const sign = Math.sign(lastInput);
+ if (sign === 0) return;
+
+ appHistory.pop();
+ let newInput;
+
+ if (sign === 1) {
+ newInput = -Math.abs(lastInput);
+ } else if (sign === -1) {
+ newInput = Math.abs(lastInput);
+ }
+
+ appHistory.push(`${newInput}`);
+ updateDisplay(`${newInput}`);
+}
+
+/**
+ * Handle decimal.
+ */
+function handleDecimal() {
+ const lastInput = getLastHistoryInput();
+
+ if (lastInput.indexOf(".") === -1) {
+ appHistory.pop();
+ const newInput = `${lastInput}.`;
+ appHistory.push(newInput);
+ updateDisplay(newInput);
+ }
+}
+
+/**
+ * Clear the last input.
+ */
+function clear() {
+ appHistory.pop();
+
+ if (appHistory.length === 0) {
+ appHistory.push("0");
+ updateDisplay("0");
+ } else {
+ const reversedHistory = appHistory.slice(0).reverse();
+ const lastNumericInput = reversedHistory.find((input) => isNumeric(input));
+ updateDisplay(lastNumericInput);
+
+ let lastInput = getLastHistoryInput();
+
+ while (lastNumericInput !== lastInput) {
+ appHistory.pop();
+ lastInput = getLastHistoryInput();
+ }
+ }
+}
+
+/**
+ * Reset the calculator.
+ */
+function clearAll() {
+ appHistory.length = 0;
+ appHistory.push("0");
+ updateDisplay("0");
+}
+
+/**
+ * Dispatch the event to the right function.
+ * @param {MouseEvent} e - The click event.
+ */
+function dispatch(e) {
+ const id = e.target.id;
+ const type = id.split("-")[0];
+ const value = e.target.textContent.trim();
+
+ switch (type) {
+ case "digit":
+ handleDigits(value);
+ break;
+ case "operation":
+ handleOperation(value);
+ break;
+ case "sign":
+ handleNumberSign();
+ break;
+ case "dot":
+ handleDecimal();
+ break;
+ case "clear":
+ clear();
+ break;
+ case "clearall":
+ clearAll();
+ break;
+ default:
+ break;
+ }
+}
+
+/**
+ * Listen all calculator buttons.
+ */
+function listen() {
+ const buttons = document.getElementsByClassName("btn");
+ const buttonsArray = Array.from(buttons);
+ buttonsArray.forEach((btn) => {
+ btn.addEventListener("click", dispatch);
+ });
+}
+
+listen();
diff --git a/public/projects/js-small-apps/calculator/index.html b/public/projects/js-small-apps/calculator/index.html
new file mode 100644
index 0000000..a93f0c4
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/index.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Calculator</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="header">
+ <h1 class="branding">Calculator</h1>
+ </header>
+ <main class="main">
+ <div class="calculator">
+ <div class="calculator__display">0</div>
+ <div class="calculator__entry-pad">
+ <div class="calculator__clear">
+ <button type="button" id="clear" class="btn btn--clear">C</button>
+ <button type="button" id="clearall" class="btn btn--clear">
+ AC
+ </button>
+ </div>
+ <div class="calculator__digits">
+ <button type="button" id="digit-9" class="btn btn--digits">
+ 9
+ </button>
+ <button type="button" id="digit-8" class="btn btn--digits">
+ 8
+ </button>
+ <button type="button" id="digit-7" class="btn btn--digits">
+ 7
+ </button>
+ <button type="button" id="digit-6" class="btn btn--digits">
+ 6
+ </button>
+ <button type="button" id="digit-5" class="btn btn--digits">
+ 5
+ </button>
+ <button type="button" id="digit-4" class="btn btn--digits">
+ 4
+ </button>
+ <button type="button" id="digit-3" class="btn btn--digits">
+ 3
+ </button>
+ <button type="button" id="digit-2" class="btn btn--digits">
+ 2
+ </button>
+ <button type="button" id="digit-1" class="btn btn--digits">
+ 1
+ </button>
+ <button type="button" id="digit-0" class="btn btn--digits">
+ 0
+ </button>
+ <button type="button" id="sign" class="btn btn--digits">+/-</button>
+ <button type="button" id="dot" class="btn btn--digits">.</button>
+ </div>
+ <div class="calculator__operations">
+ <button
+ type="button"
+ id="operation-divide"
+ class="btn btn--operation"
+ >
+ /
+ </button>
+ <button
+ type="button"
+ id="operation-multiply"
+ class="btn btn--operation"
+ >
+ *
+ </button>
+ <button
+ type="button"
+ id="operation-minus"
+ class="btn btn--operation"
+ >
+ -
+ </button>
+ <button
+ type="button"
+ id="operation-plus"
+ class="btn btn--operation"
+ >
+ +
+ </button>
+ <button
+ type="button"
+ id="operation-equal"
+ class="btn btn--operation"
+ >
+ =
+ </button>
+ </div>
+ </div>
+ </div>
+ </main>
+ <footer class="footer">
+ <p class="copyright">Calculator. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/calculator/style.css b/public/projects/js-small-apps/calculator/style.css
new file mode 100644
index 0000000..89b5b31
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/style.css
@@ -0,0 +1,147 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+ Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ font-size: 16px;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+}
+
+.header,
+.main,
+.footer {
+ width: min(calc(100vw - 2rem), 350px);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.header,
+.main {
+ border: 3px solid hsl(219, 64%, 35%);
+ border-radius: 5px;
+ box-shadow: 2px 2px 0 0 hsl(219, 64%, 35%), 2px 2px 2px 0 hsl(219, 64%, 30%),
+ 3px 3px 3px 0 hsla(219, 64%, 25%, 0.65);
+}
+
+.header {
+ background: hsl(219, 64%, 35%);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ color: hsl(0, 0%, 100%);
+ margin-top: 2rem;
+ padding: 0.5rem 1rem;
+}
+
+.branding {
+ font-size: 1.5rem;
+ font-variant: small-caps;
+ letter-spacing: 1px;
+ text-align: center;
+ text-shadow: 1px 1px 0 hsl(0, 0%, 65%), 2px 2px 2px hsl(0, 0%, 0%);
+}
+
+.main {
+ background: hsl(0, 0%, 97%);
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+
+ margin-bottom: 1rem;
+}
+
+.footer {
+ font-size: 0.9rem;
+ text-align: center;
+ margin-top: auto;
+ padding: 1rem 0;
+}
+
+.calculator {
+ padding: 1rem;
+}
+
+.calculator__display {
+ background: hsl(0, 0%, 100%);
+ border: 1px solid hsl(0, 0%, 60%);
+ border-radius: 2px;
+ box-shadow: inset 0 0 2px 0 hsl(0, 0%, 70%), 0 0 0 1px hsl(0, 0%, 75%);
+ font-size: clamp(2rem, 3vw, 2.5rem);
+ font-weight: 600;
+ text-align: right;
+ width: 100%;
+ margin-bottom: 1rem;
+ padding: 0.2rem 1rem;
+}
+
+.calculator__entry-pad {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: 1fr 6fr;
+ gap: 1rem;
+ justify-items: end;
+}
+
+.calculator__clear {
+ grid-column: 3 / 5;
+ grid-row: 1;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-end;
+ gap: 1rem;
+}
+
+.calculator__digits {
+ grid-column: 1 / 4;
+ grid-row: 2;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.calculator__operations {
+ grid-column: 4;
+ grid-row: 2;
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1rem;
+}
+
+.btn {
+ display: block;
+ padding: clamp(0.1rem, 5vw, 0.3rem) clamp(1rem, 6vw, 1.5rem);
+ background: hsl(0, 0%, 95%);
+ border: 1px solid hsl(0, 0%, 85%);
+ border-radius: 3px;
+ box-shadow: 0 0 2px hsl(0, 0%, 80%), 0 0 0 2px hsl(0, 0%, 60%),
+ 1px 1px 0 3px hsl(0, 0%, 50%);
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ cursor: pointer;
+ transition: all 0.15s ease-in-out 0s;
+}
+
+.btn:hover,
+.btn:focus {
+ background: hsl(0, 0%, 100%);
+}
+
+.btn:focus {
+ outline: 3px solid hsl(219, 64%, 35%);
+}
+
+.btn:active {
+ background: hsl(0, 0%, 90%);
+ box-shadow: 0 0 0 hsl(0, 0%, 80%), 0 0 0 2px hsl(0, 0%, 80%),
+ 0 0 0 3px hsl(0, 0%, 50%);
+ outline: none;
+ transform: translateX(2px) translateY(2px) scale(0.96);
+}