diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:11:50 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:15:08 +0100 |
| commit | 73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch) | |
| tree | c8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/js-small-apps | |
| parent | b01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff) | |
chore!: restructure repo
I separated public files from the config/dev files. It improves repo
readability.
I also moved dotenv helper to public/inc directory and extract the
Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/js-small-apps')
50 files changed, 8220 insertions, 0 deletions
diff --git a/public/projects/js-small-apps/.gitignore b/public/projects/js-small-apps/.gitignore new file mode 100644 index 0000000..a807e87 --- /dev/null +++ b/public/projects/js-small-apps/.gitignore @@ -0,0 +1,6 @@ +# Dependencies +node_modules +yarn-error* + +# Misc +.vscode diff --git a/public/projects/js-small-apps/.husky/commit-msg b/public/projects/js-small-apps/.husky/commit-msg new file mode 100755 index 0000000..e8511ea --- /dev/null +++ b/public/projects/js-small-apps/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install commitlint --edit $1 diff --git a/public/projects/js-small-apps/LICENSE b/public/projects/js-small-apps/LICENSE new file mode 100644 index 0000000..fb17359 --- /dev/null +++ b/public/projects/js-small-apps/LICENSE @@ -0,0 +1,21 @@ +MIT License
+
+Copyright (c) 2021 Armand Philippot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/public/projects/js-small-apps/README.md b/public/projects/js-small-apps/README.md new file mode 100644 index 0000000..f167e65 --- /dev/null +++ b/public/projects/js-small-apps/README.md @@ -0,0 +1,17 @@ +# JS Small Apps + +A collection of small apps and exercises implemented with Javascript. + +## Description + +I want to keep track of some small apps or exercises but I don't think they deserve their own repository. So, I decided to gather them inside this repo. + +Most of the app ideas come from [@florinpop17's app-ideas repo](https://github.com/florinpop17/app-ideas). + +## Preview + +You can see a live preview of the apps here: https://demo.armandphilippot.com/ + +## License + +This project is open-source and available under the [MIT License](./LICENSE). diff --git a/public/projects/js-small-apps/bin2dec/README.md b/public/projects/js-small-apps/bin2dec/README.md new file mode 100644 index 0000000..01a3756 --- /dev/null +++ b/public/projects/js-small-apps/bin2dec/README.md @@ -0,0 +1,13 @@ +# Bin2Dec + +An app to convert binary strings to decimal number. + +You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Bin2Dec-App.md + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#bin2dec + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/bin2dec/app.js b/public/projects/js-small-apps/bin2dec/app.js new file mode 100644 index 0000000..ccdaee0 --- /dev/null +++ b/public/projects/js-small-apps/bin2dec/app.js @@ -0,0 +1,74 @@ +function setErrorBox() { + const main = document.querySelector(".main"); + const form = document.querySelector(".form"); + const errorBox = document.createElement("div"); + errorBox.classList.add("error-box"); + main.insertBefore(errorBox, form); +} + +function getErrorBox() { + let errorBox = document.querySelector(".error-box"); + + if (!errorBox) { + setErrorBox(); + errorBox = document.querySelector(".error-box"); + } + + return errorBox; +} + +function removeErrorBox() { + const errorBox = getErrorBox(); + errorBox.remove(); +} + +function printError(error) { + const errorBox = getErrorBox(); + errorBox.textContent = error; +} + +function isValidInput(key) { + return key === "0" || key === "1"; +} + +function hasInvalidChar(string) { + const regex = /(?![01])./g; + const invalid = string.search(regex); + return invalid === -1 ? false : true; +} + +function handleInput(value) { + if (hasInvalidChar(value)) { + const error = "Error: valid characters are 0 or 1."; + printError(error); + } else { + removeErrorBox(); + } +} + +function convertBinToDec(bin) { + let result = 0; + + for (const char of bin) { + result = result * 2 + Number(char); + } + + return result; +} + +function handleSubmit(e) { + e.preventDefault(); + const input = document.querySelector(".form__input"); + const result = document.getElementById("result"); + result.textContent = convertBinToDec(input.value); +} + +function init() { + const form = document.querySelector(".form"); + const input = document.querySelector(".form__input"); + handleInput(input.value); + form.addEventListener("submit", handleSubmit); + input.addEventListener("keyup", (e) => handleInput(e.target.value)); +} + +init(); diff --git a/public/projects/js-small-apps/bin2dec/index.html b/public/projects/js-small-apps/bin2dec/index.html new file mode 100644 index 0000000..dd8951d --- /dev/null +++ b/public/projects/js-small-apps/bin2dec/index.html @@ -0,0 +1,41 @@ +<!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>Bin2Dec</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="header"><h1 class="branding">Bin2Dec</h1></header> + <main class="main"> + <form action="#" method="POST" class="form"> + <fieldset class="form__fieldset"> + <legend class="form__legend"> + Convert a binary number to decimal + </legend> + <label class="form__label">Binary:</label> + <input + type="text" + name="binary-string" + id="input-field" + value="" + required + pattern="[0-1]{1,}" + class="form__input" + /> + <button type="submit" class="btn">Convert</button> + </fieldset> + <div class="result-box"> + <p class="result-box__label">Decimal:</p> + <p id="result"></p> + </div> + </form> + </main> + <footer class="footer"> + <p class="copyright">Bin2Dec. MIT 2021. Armand Philippot.</p> + </footer> + <script src="app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/bin2dec/style.css b/public/projects/js-small-apps/bin2dec/style.css new file mode 100644 index 0000000..22db30a --- /dev/null +++ b/public/projects/js-small-apps/bin2dec/style.css @@ -0,0 +1,118 @@ +*, +*::after, +*::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #fff; + color: #000; + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem; + line-height: 1.618; + display: flex; + flex-flow: column nowrap; + min-height: 100vh; +} + +.header, +.main, +.footer { + width: min(calc(100vw - 2rem), 80ch); + margin-left: auto; + margin-right: auto; +} + +.header { + padding: 2rem 0 3rem; + text-align: center; +} + +.main { + flex: 1; + display: flex; + flex-flow: column nowrap; + align-content: flex-start; +} + +.footer { + margin-top: 2rem; + padding: 1rem 0; +} + +.copyright { + font-size: 0.9rem; + text-align: center; +} + +.form { + border: solid 2px hsl(219, 64%, 35%); + border-radius: 5px; + padding: 1rem; + margin: 0 auto; +} + +.form__fieldset { + border: none; + display: flex; + flex-flow: row wrap; + align-items: center; + gap: 0.5rem; +} + +.form__legend { + color: hsl(219, 64%, 35%); + font-weight: 600; + font-size: 1.1rem; + margin-bottom: 2rem; +} + +.form__label { + cursor: pointer; + font-weight: 600; +} + +.form__input { + border: 1px solid hsl(219, 64%, 35%); + font: inherit; + line-height: inherit; + padding: 0.2rem 0.5rem; +} + +.btn { + background: #fff; + border: 2px solid hsl(219, 64%, 35%); + color: hsl(219, 64%, 35%); + font: inherit; + font-weight: 600; + line-height: inherit; + padding: 0.2rem 0.5rem; + cursor: pointer; +} + +.btn:hover, +.btn:focus { + background: hsl(219, 64%, 35%); + color: #fff; +} + +.error-box { + border: 1px solid hsl(0, 75%, 38%); + color: hsl(0, 75%, 38%); + font-weight: 600; + margin: 0 auto 2rem; + padding: 1rem; + width: max-content; +} + +.result-box { + display: flex; + gap: 0.5rem; + margin-top: 2rem; +} + +.result-box__label { + font-weight: 600; +} diff --git a/public/projects/js-small-apps/budget-app/README.md b/public/projects/js-small-apps/budget-app/README.md new file mode 100644 index 0000000..fe8f09f --- /dev/null +++ b/public/projects/js-small-apps/budget-app/README.md @@ -0,0 +1,17 @@ +# Budget app + +A simple budget app. + +## Description + +You can define some categories for your expenses and incomes. Then you can add/edit/remove each type of transaction. + +The app will show you the remaining budget and the total expenses based on the initial budget. + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#budget-app + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/budget-app/app.js b/public/projects/js-small-apps/budget-app/app.js new file mode 100644 index 0000000..2fb0cba --- /dev/null +++ b/public/projects/js-small-apps/budget-app/app.js @@ -0,0 +1,311 @@ +import BudgetApp from "./lib/class-budget-app.js"; +import Notification from "./lib/class-notification.js"; +import getCurrencyFormat from "./lib/utils/currency.js"; + +const app = new BudgetApp("Budget", "Anonymous"); +const ui = { + budget: { + remaining: document.getElementById("budget-remaining"), + spent: document.getElementById("budget-spent"), + }, + buttons: { + categories: { + add: document.querySelector(".manage-categories .btn--add"), + delete: document.querySelector(".manage-categories .btn--delete"), + rename: document.querySelector(".manage-categories .btn--rename"), + }, + register: document.querySelector(".register .btn--register"), + reset: document.querySelector(".footer .btn--reset"), + transactions: { + update: document.querySelector(".manage-transactions .btn--update"), + }, + }, + form: { + categories: { + add: document.getElementById("add-category"), + rename: document.getElementById("rename-category"), + select: document.getElementById("select-category"), + }, + register: { + budget: document.getElementById("register-budget"), + language: document.getElementById("register-locale"), + username: document.getElementById("register-username"), + }, + transactions: { + amount: document.getElementById("transaction-amount"), + category: document.getElementById("transaction-category"), + date: document.getElementById("transaction-date"), + id: document.getElementById("transaction-id"), + name: document.getElementById("transaction-name"), + type: document.getElementById("transaction-type"), + }, + }, + history: { + body: document.querySelector(".app__history .table__body"), + }, + title: document.querySelector(".branding__title"), +}; + +function initApp() { + const register = document.querySelector(".register"); + const application = document.querySelector(".app"); + const budget = ui.form.register.budget.value; + const locale = ui.form.register.language.value; + const username = ui.form.register.username.value; + + if (budget && locale && username) { + app.user.username = username; + app.user.budget = budget; + app.user.locale = locale; + register.style.display = "none"; + application.style.display = "block"; + } else { + notify("You must complete all fields!", "error", 3000); + } +} + +function notify(message, type, duration, position = "bottom") { + const notification = new Notification(message, type); + notification.duration = duration; + notification.position = position; + return notification.notify(); +} + +function getSelectOptions(select, options) { + select.innerHTML = ""; + options.forEach((option) => { + select.add(new Option(option.name, option.id)); + }); + + return select; +} + +function findName(id, array) { + const object = array.find((item) => item.id === Number(id)); + if (object) { + return object.name; + } else { + return "(deleted)"; + } +} + +function resetTransactionForm() { + ui.form.transactions.amount.value = ""; + ui.form.transactions.category.value = ""; + ui.form.transactions.date.value = ""; + ui.form.transactions.id.value = ""; + ui.form.transactions.name.value = ""; + ui.form.transactions.type.value = ""; +} + +function setTransactionForm(transaction) { + ui.form.transactions.amount.value = transaction.amount; + ui.form.transactions.category.value = transaction.category; + ui.form.transactions.date.valueAsDate = transaction.date; + ui.form.transactions.id.value = transaction.id; + ui.form.transactions.name.value = transaction.name; + ui.form.transactions.type.value = transaction.type; +} + +function manageHistory(target) { + const tr = target.parentElement.parentElement; + const transactionId = Number(tr.id.replace("transaction-", "")); + const array = tr.classList.contains("table__row--expense") + ? app.expenses + : app.incomes; + if (target.classList.contains("btn--delete")) { + const tbody = tr.parentElement; + app.remove(transactionId, array); + updateBudget(); + updateHistory(tbody); + } else if (target.classList.contains("btn--edit")) { + const index = array.findIndex( + (transaction) => transaction.id === transactionId + ); + setTransactionForm(array[index]); + } +} + +function getTransactionButton(type) { + const btn = document.createElement("button"); + let text = ""; + + switch (type) { + case "delete": + text = "Delete"; + break; + case "edit": + text = "Edit"; + break; + default: + break; + } + + btn.textContent = text; + btn.classList.add("btn", `btn--${type}`); + btn.addEventListener("click", (event) => manageHistory(event.target)); + + return btn; +} + +function getTransactionCell(data = "") { + const td = document.createElement("td"); + td.classList.add("table__item"); + td.textContent = data; + + return td; +} + +function getTransactionRow(transaction) { + const tr = document.createElement("tr"); + const amount = + transaction.type === "expense" + ? transaction.amount * -1 + : transaction.amount; + const localizedAmount = getCurrencyFormat(amount, app.user.locale); + const categoryName = findName(transaction.category, app.categories); + const date = transaction.date.toLocaleDateString(app.user.locale); + + const dateCell = getTransactionCell(date); + const nameCell = getTransactionCell(transaction.name); + const categoryCell = getTransactionCell(categoryName); + const typeCell = getTransactionCell(transaction.type); + const amountCell = getTransactionCell(localizedAmount); + const manageCell = getTransactionCell(); + const editButton = getTransactionButton("edit"); + const deleteButton = getTransactionButton("delete"); + + manageCell.append(editButton, deleteButton); + tr.classList.add("table__row", `table__row--${transaction.type}`); + tr.id = `transaction-${transaction.id}`; + tr.append(dateCell, nameCell, categoryCell, typeCell, amountCell, manageCell); + + return tr; +} + +function getHistory(tbody) { + const transactions = app.getOrderedTransactions("oldest"); + transactions.forEach((transaction) => { + tbody.appendChild(getTransactionRow(transaction)); + }); +} + +function updateBudget() { + app.updateUserBudget(); + ui.budget.remaining.textContent = getCurrencyFormat( + app.user.budget.remaining(), + app.user.locale + ); + ui.budget.spent.textContent = getCurrencyFormat( + app.user.budget.spent, + app.user.locale + ); +} + +function updateCategories() { + getSelectOptions(ui.form.categories.select, app.categories); + getSelectOptions(ui.form.transactions.category, app.categories); +} + +function updateHistory() { + ui.history.body.innerHTML = ""; + getHistory(ui.history.body, app); +} + +function updateAll() { + ui.title.textContent = `${app.user.username} ${app.title}`; + updateBudget(); + updateCategories(); + updateHistory(); +} + +function listen() { + for (const [name, element] of Object.entries(ui.buttons.categories)) { + element.addEventListener("click", (event) => { + event.preventDefault(); + const id = Number(ui.form.categories.select.value); + + switch (name) { + case "add": + if (ui.form.categories.add.value) { + app.addCategory(ui.form.categories.add.value); + ui.form.categories.add.value = ""; + } else { + notify("Category name must be filled!", "error", 3000); + } + break; + case "delete": + if (id) { + app.remove(id, app.categories); + updateHistory(); + } else { + notify("A category must be selected!", "error", 3000); + } + break; + case "rename": + const newName = ui.form.categories.rename.value; + if (newName && id) { + app.renameCategory(id, newName); + updateHistory(); + ui.form.categories.rename.value = ""; + } else { + notify( + "You need to select a category and enter a new name first!", + "error", + 3000 + ); + } + break; + default: + break; + } + + updateCategories(); + }); + } + + ui.buttons.transactions.update.addEventListener("click", (event) => { + event.preventDefault(); + const transactionId = Number(ui.form.transactions.id.value); + const transaction = { + date: ui.form.transactions.date.value, + name: ui.form.transactions.name.value, + type: ui.form.transactions.type.value, + category: ui.form.transactions.category.value, + amount: ui.form.transactions.amount.value, + }; + const error = []; + for (const value in transaction) { + const element = transaction[value]; + !element ? error.push(value) : ""; + } + if (error.length === 0) { + transactionId + ? app.editTransaction({ id: transactionId, ...transaction }) + : app.addTransaction(transaction); + updateBudget(); + updateHistory(); + resetTransactionForm(); + } else { + const errorMsg = `These fields (${error.join(", ")}) must be filled!`; + notify(errorMsg, "error", 3000); + } + }); + + ui.buttons.register.addEventListener("click", (event) => { + event.preventDefault(); + initApp(); + updateAll(); + }); + + ui.buttons.reset.addEventListener("click", (event) => { + event.preventDefault(); + if (confirm("Are you sure?")) { + notify("Reset!", "warning", 2000); + app.reset(); + } + updateAll(); + }); +} + +listen(); diff --git a/public/projects/js-small-apps/budget-app/index.html b/public/projects/js-small-apps/budget-app/index.html new file mode 100644 index 0000000..90f5d12 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/index.html @@ -0,0 +1,232 @@ +<!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>Budget App</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="branding"> + <h1 class="branding__title">Budget App</h1> + </header> + <main class="main"> + <div class="register"> + <form action="#" class="register__form form"> + <p class="form__item"> + <label for="register-username" class="form__label" + >Enter a username:</label + > + <input + type="text" + id="register-username" + name="register-username" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="register-budget" class="form__label" + >Enter a budget:</label + > + <input + type="number" + id="register-budget" + name="register-budget" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="register-locale" class="form__label" + >Select a language:</label + > + <select + name="register-locale" + id="register-locale" + class="form__select" + required + > + <option value="en-US">Anglais</option> + <option value="fr-FR">Français</option> + </select> + </p> + <button type="submit" class="btn btn--register">Register</button> + </form> + </div> + <div class="app"> + <div class="app__management"> + <div class="app__categories"> + <h2 class="app__title">Categories</h2> + <form action="#" class="manage-categories form"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Add a new category</legend> + <p class="form__item"> + <label for="add-category" class="form__label" + >Category name:</label + > + <input + type="text" + name="" + id="add-category" + class="form__field" + required + /> + <button type="submit" class="form__submit btn btn--add"> + Add + </button> + </p> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Manage existing categories</legend> + <p class="form__item"> + <label for="select-category" class="form__label" + >Select a category:</label + > + <select + name="select-category" + id="select-category" + class="form__select" + > + <option value="undefined" class="form__option"> + Undefined + </option> + </select> + <button type="submit" class="form__submit btn btn--delete"> + Delete + </button> + </p> + <p class="form__item"> + <label for="rename-category" class="form__label" + >New name:</label + > + <input + type="text" + name="rename-category" + id="rename-category" + class="form__field" + /> + <button type="submit" class="form__submit btn btn--rename"> + Rename + </button> + </p> + </fieldset> + </form> + </div> + <div class="app__transactions"> + <h2 class="app__title">Transactions</h2> + <form action="#" class="manage-transactions form"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Manage transactions</legend> + <p class="form__item"> + <label for="transaction-date" class="form__label" + >Select a date:</label + > + <input + type="date" + name="transaction-date" + id="transaction-date" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="" class="form__label">Choose a type:</label> + <select + name="transaction-type" + id="transaction-type" + class="form__select" + > + <option value=""></option> + <option value="expense">Expense</option> + <option value="income">Income</option> + </select> + </p> + <p class="form__item"> + <label for="" class="form__label">Choose a category:</label> + <select + name="transaction-category" + id="transaction-category" + class="form__select" + > + <option value="undefined">Undefined</option> + </select> + </p> + <p class="form__item"> + <label for="transaction-name" class="form__label" + >Name:</label + > + <input + type="text" + name="transaction-name" + id="transaction-name" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="transaction-amount" class="form__label" + >Amount:</label + > + <input + type="number" + name="transaction-amount" + id="transaction-amount" + step="0.01" + class="form__field" + required + /> + </p> + <input + type="hidden" + name="transaction-id" + id="transaction-id" + /> + <button type="submit" class="form__submit btn btn--update"> + Update + </button> + </fieldset> + </form> + </div> + <div class="app__budget"> + <h2 class="app__title">Budget state</h2> + <table class="table"> + <thead class="table__header"> + <tr> + <td class="table__item">Spent</td> + <td class="table__item">Remaining</td> + </tr> + </thead> + <tbody class="table__body"> + <tr> + <td id="budget-spent" class="table__item">0</td> + <td id="budget-remaining" class="table__item">0</td> + </tr> + </tbody> + </table> + </div> + </div> + <div class="app__history"> + <h2 class="app__title">History</h2> + <table class="table"> + <thead class="table__header"> + <td class="table__item">Date</td> + <td class="table__item">Name</td> + <td class="table__item">Category</td> + <td class="table__item">Type</td> + <td class="table__item">Amount</td> + <td class="table__item">Manage</td> + </thead> + <tbody class="table__body"></tbody> + </table> + </div> + </div> + </main> + <footer class="footer container"> + <button type="button" class="btn btn--reset">Reset</button> + <p class="footer__copyright">Budget App. MIT 2021. Armand Philippot.</p> + </footer> + <script src="app.js" type="module"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/budget-app/lib/class-budget-app.js b/public/projects/js-small-apps/budget-app/lib/class-budget-app.js new file mode 100644 index 0000000..45097a0 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-budget-app.js @@ -0,0 +1,144 @@ +import Category from "./class-category.js"; +import Transaction from "./class-transaction.js"; +import User from "./class-user.js"; + +class BudgetApp { + #title = "Budget App"; + #categoryId = 1; + #categories = []; + #transactionId = 1; + #incomes = []; + #expenses = []; + #userId = 1; + #user = {}; + + constructor(title, username) { + this.#title = title; + this.#user = new User(this.#userId++, username); + } + + set title(string) { + this.#title = string; + } + + get title() { + return this.#title; + } + + set categories(array) { + this.#categories = array; + } + + get categories() { + return this.#categories; + } + + set incomes(array) { + this.#incomes = array; + } + + get incomes() { + return this.#incomes; + } + + set expenses(array) { + this.#expenses = array; + } + + get expenses() { + return this.#expenses; + } + + set user(username) { + this.#user = new User(this.#userId++, username); + } + + get user() { + return this.#user; + } + + remove(id, from) { + const index = from.findIndex((object) => object.id === id); + from.splice(index, 1); + } + + addCategory(name) { + this.#categories.push(new Category(this.#categoryId++, name)); + } + + renameCategory(id, newName) { + const index = this.categories.findIndex((object) => object.id === id); + this.categories[index].name = newName; + } + + addTransaction(transaction) { + const array = + transaction.type === "income" ? this.#incomes : this.#expenses; + array.push( + new Transaction( + this.#transactionId++, + transaction.date, + transaction.name, + transaction.type, + transaction.category, + transaction.amount + ) + ); + } + + editTransaction(transaction) { + const array = transaction.type === "income" ? this.incomes : this.expenses; + const index = array.findIndex((object) => { + return object.id === Number(transaction.id); + }); + if (index !== -1) { + array[index] = new Transaction(...Object.values(transaction)); + } else { + const oldArray = array === this.incomes ? this.expenses : this.incomes; + array.push(new Transaction(...Object.values(transaction))); + this.remove(transaction.id, oldArray); + } + } + + getOrderedTransactions(order) { + const transactions = [...this.expenses, ...this.incomes]; + + switch (order) { + case "newest": + transactions.sort((a, b) => b.date - a.date); + break; + case "oldest": + transactions.sort((a, b) => a.date - b.date); + break; + default: + break; + } + + return transactions; + } + + total(transaction) { + const array = transaction === "expense" ? this.#expenses : this.#incomes; + let total = 0; + array.forEach((item) => { + total += item.amount; + }); + return total; + } + + updateUserBudget() { + this.user.budget.spent = this.total("expense"); + this.user.budget.profit = this.total("income"); + } + + reset() { + this.#categoryId = 1; + this.#transactionId = 1; + this.#categories = []; + this.#incomes = []; + this.#expenses = []; + this.updateUserBudget(); + } +} + +export default BudgetApp; diff --git a/public/projects/js-small-apps/budget-app/lib/class-budget.js b/public/projects/js-small-apps/budget-app/lib/class-budget.js new file mode 100644 index 0000000..cde22fd --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-budget.js @@ -0,0 +1,44 @@ +/** + * Budget class + * + * Create a new budget. + */ +class Budget { + #initial = 0; + #spent = 0; + #profit = 0; + + constructor(initial) { + this.#initial = Number.parseFloat(initial); + } + + set initial(number) { + this.#initial = Number.parseFloat(number); + } + + get initial() { + return this.#initial; + } + + set spent(number) { + this.#spent = Number.parseFloat(number); + } + + get spent() { + return this.#spent; + } + + set profit(number) { + this.#profit = Number.parseFloat(number); + } + + get profit() { + return this.#profit; + } + + remaining() { + return this.initial + this.profit - this.spent; + } +} + +export default Budget; diff --git a/public/projects/js-small-apps/budget-app/lib/class-category.js b/public/projects/js-small-apps/budget-app/lib/class-category.js new file mode 100644 index 0000000..7b2f1b2 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-category.js @@ -0,0 +1,37 @@ +/** + * Category class. + * + * Create a new category with id, name and attachments. + */ +class Category { + #id = 0; + #name = ""; + #attachments = []; + + constructor(id, name) { + this.#id = Number(id); + this.#name = name; + } + + set name(name) { + this.#name = name; + } + + get name() { + return this.#name; + } + + get id() { + return this.#id; + } + + set attachments(attachment) { + this.#attachments.push(Number(attachment)); + } + + get attachments() { + return this.#attachments; + } +} + +export default Category; diff --git a/public/projects/js-small-apps/budget-app/lib/class-notification.js b/public/projects/js-small-apps/budget-app/lib/class-notification.js new file mode 100644 index 0000000..17a32a0 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-notification.js @@ -0,0 +1,89 @@ +class Notification { + #id = 0; + #title = ""; + #message = ""; + #type = ""; + #duration = 0; + #position = ""; + + constructor(message, type) { + this.#message = message; + this.#type = type; + } + + get id() { + return this.#id; + } + + set title(string) { + this.#title = string; + } + + get title() { + return this.#title; + } + + set message(text) { + this.#message = text; + } + + get message() { + return this.#message; + } + + set type(string) { + this.#type = string; + } + + get type() { + return this.#type; + } + + set duration(number) { + this.#duration = number; + } + + get duration() { + return this.#duration; + } + + set position(string) { + this.#position = string; + } + + get position() { + return this.#position; + } + + #getWrapper() { + let wrapper = document.getElementById("notifications-center"); + + if (!wrapper) { + wrapper = document.createElement("div"); + wrapper.id = "notifications-center"; + wrapper.classList = "notifications"; + document.body.appendChild(wrapper); + } + + return wrapper; + } + + notify() { + const notification = document.createElement("div"); + notification.textContent = this.message; + notification.classList.add("notification", `notification--${this.type}`); + + const wrapper = this.#getWrapper(); + document.body.style.position = "relative"; + wrapper.style.cssText = `position: fixed;${this.position}: 1rem;right: 1rem;display: flex;flex-flow: column wrap;gap: 1rem;`; + wrapper.appendChild(notification); + + if (this.duration && Number(this.duration) !== 0) { + setTimeout(() => { + notification.remove(); + }, this.duration); + } + } +} + +export default Notification; diff --git a/public/projects/js-small-apps/budget-app/lib/class-transaction.js b/public/projects/js-small-apps/budget-app/lib/class-transaction.js new file mode 100644 index 0000000..6807372 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-transaction.js @@ -0,0 +1,76 @@ +/** + * Transaction class + * + * Create a new transaction with id, date, name, type, category and amount. + */ +class Transaction { + #id = 0; + #date = new Date(); + #name = ""; + #type = ""; + #category = 0; + #amount = 0; + + constructor(id, date, name, type, category, amount) { + this.#id = Number(id); + this.#date = new Date(date); + this.#name = name; + this.#type = type; + this.#category = Number(category); + this.#amount = Number.parseFloat(amount); + } + + get id() { + return this.#id; + } + + set date(datetime) { + this.#date = datetime; + } + + get date() { + return this.#date; + } + + set name(string) { + this.#name = string; + } + + get name() { + return this.#name; + } + + set type(string) { + this.#type = string; + } + + get type() { + return this.#type; + } + + set category(string) { + this.#category = string; + } + + get category() { + return this.#category; + } + + set amount(number) { + this.#amount = number; + } + + get amount() { + return this.#amount; + } + + update(date, name, type, category, amount) { + this.date = date; + this.name = name; + this.type = type; + this.category = category; + this.amount = amount; + } +} + +export default Transaction; diff --git a/public/projects/js-small-apps/budget-app/lib/class-user.js b/public/projects/js-small-apps/budget-app/lib/class-user.js new file mode 100644 index 0000000..e0e137b --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-user.js @@ -0,0 +1,79 @@ +import Budget from "./class-budget.js"; + +class User { + #id = 0; + #username = "Anonymous"; + #firstName = "John"; + #lastName = "Doe"; + #role = "admin"; + #locale = "en-US"; + #accountCreation = new Date(); + #budget = 0; + + constructor(id, username) { + this.#id = id; + this.#username = username; + } + + get id() { + return this.#id; + } + + set username(name) { + this.#username = name; + } + + get username() { + return this.#username; + } + + set firstName(name) { + this.#firstName = name; + } + + get firstName() { + return this.#firstName; + } + + set lastName(name) { + this.#lastName = name; + } + + get lastName() { + return this.#lastName; + } + + set role(string) { + this.#role = string; + } + + get role() { + return this.#role; + } + + set locale(code) { + this.#locale = code; + } + + get locale() { + return this.#locale; + } + + get accountCreation() { + return this.#accountCreation; + } + + set budget(number) { + this.#budget = new Budget(number); + } + + get budget() { + return this.#budget; + } + + name() { + return `${this.#firstName} ${this.#lastName}`; + } +} + +export default User; diff --git a/public/projects/js-small-apps/budget-app/lib/utils/currency.js b/public/projects/js-small-apps/budget-app/lib/utils/currency.js new file mode 100644 index 0000000..04d3ad0 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/utils/currency.js @@ -0,0 +1,47 @@ +/** + * Convert a number to fr_FR locale. + * @param {Number} number A number to format. + * @returns A number formatted with fr_FR locale. + */ +const getCurrencyFR = (number) => { + const formatted = + Number.parseFloat(number) + .toFixed(2) + .replace(".", ",") + .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1 ") + " €"; + return formatted; +}; + +/** + * Convert a number to en_US locale. + * @param {Number} number A number to format. + * @returns A number formatted with en_US locale. + */ +const getCurrencyUS = (number) => { + const formatted = + "$" + + Number.parseFloat(number) + .toFixed(2) + .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,"); + return formatted; +}; + +/** + * Get a number formatted based on a locale. + * @param {Number} number A number to format. + * @param {String} format A language code. + * @returns A formatted number. + */ +const getCurrencyFormat = (number, format) => { + switch (format) { + case "fr-FR": + return getCurrencyFR(number); + case "en-US": + return getCurrencyUS(number); + default: + console.log("Not supported!"); + break; + } +}; + +export default getCurrencyFormat; diff --git a/public/projects/js-small-apps/budget-app/style.css b/public/projects/js-small-apps/budget-app/style.css new file mode 100644 index 0000000..009fbb7 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/style.css @@ -0,0 +1,366 @@ +/* + * Base + */ + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: hsl(207, 52%, 95%); + color: #000; + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem; + line-height: 1.618; + display: flex; + flex-flow: column nowrap; + min-height: 100vh; + max-width: 100vw; +} + +button { + display: block; + font-family: inherit; + font-size: 100%; + line-height: 1.15; + cursor: pointer; +} + +select { + cursor: pointer; +} + +table { + width: 100%; +} + +/* + * Layout + */ + +.branding { + background: hsl(207, 35%, 90%); + border-bottom: 1px solid hsl(207, 35%, 80%); + box-shadow: 0 2px 0 1px hsl(207, 25%, 70%); + padding: 2rem 0; + margin-bottom: clamp(2rem, 3vw, 4rem); +} + +.branding__title { + color: hsl(207, 85%, 27%); + font-size: clamp(1.9rem, 5vw, 2.2rem); + text-align: center; + text-shadow: 0 0 1px hsl(207, 100%, 50%); + text-transform: uppercase; +} + +.footer { + background: hsl(207, 35%, 90%); + border-top: 1px solid hsl(207, 35%, 80%); + margin-top: clamp(3rem, 5vw, 4rem); + padding: 2rem 0 1.5rem; +} + +.footer .btn--reset { + margin-bottom: 2rem; +} + +.footer__copyright { + font-size: 0.9rem; + text-align: center; +} + +.main { + flex: 1; + padding: 0 clamp(1rem, 3vw, 2rem); +} + +.register { + display: flex; + flex-flow: column nowrap; + justify-content: center; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: hsl(202, 19%, 89%); +} + +.register__form { + margin: auto; +} + +.app { + display: none; +} + +.app__title { + background: hsl(207, 25%, 92%); + border: 1px solid hsl(207, 25%, 70%); + box-shadow: 0 2px 0 1px hsl(207, 25%, 70%); + grid-column: 1 / -1; + color: hsl(207, 85%, 27%); + font-size: clamp(1.4rem, 4vw, 1.7rem); + margin-bottom: 1rem; + padding: 0.5rem 1rem; +} + +.app__management { + display: grid; + grid-template-columns: repeat(auto-fill, min(calc(100vw - 2 * 1rem), 21rem)); + justify-content: center; + column-gap: 2rem; +} + +.app__budget { + margin-bottom: clamp(1rem, 3vw, 2rem); +} + +.form__fieldset { + border: 2px solid hsl(207, 85%, 27%); + margin-bottom: clamp(1rem, 3vw, 2rem); + padding: 0 1rem 1rem; +} + +.form__legend { + background: hsl(207, 85%, 37%); + border: 2px solid hsl(207, 85%, 27%); + color: #fff; + font-weight: 600; + width: 100%; + margin-bottom: 1rem; + padding: 0.5rem 1rem; +} + +.form__item:not(:last-child) { + margin-bottom: 1rem; +} + +.form__label { + display: block; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + margin-bottom: 0.1rem; +} + +.form__field, +.form__select { + border: 1px solid hsl(207, 85%, 27%); + box-shadow: 2px 2px 0 0 hsl(0, 0%, 20%); + font-size: inherit; + padding: 0.5rem clamp(0.5rem, 3vw, 0.8rem); + width: 100%; + transition: all 0.3s ease-in-out 0s; +} + +.form__field:hover, +.form__field:focus, +.form__select:hover, +.form__select:focus { + transform: scaleX(1.05); +} + +.form__field:focus, +.form__select:focus { + outline: none; + box-shadow: 2px 2px 0 0 hsl(0, 0%, 20%), 1px 1px 0 4px hsl(207, 85%, 60%); +} + +.manage-categories .form__item { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + column-gap: 1rem; +} + +.manage-categories .form__label { + grid-column: 1 / -1; +} + +.manage-categories .form__select, +.manage-categories .form__field { + grid-column: 1; +} + +.manage-categories .form__submit { + grid-column: 2; +} + +.table { + border: 2px solid hsl(207, 85%, 27%); + border-collapse: collapse; + width: 100%; + overflow-x: auto; +} + +.table__header { + background: hsl(207, 85%, 37%); + color: #fff; + font-weight: 600; +} + +.table__item { + border: 1px solid hsl(207, 85%, 27%); + padding: 0.5rem 0.7rem; + width: max-content; +} + +.table__item .btn { + display: inline-flex; + margin: 0.5rem; +} + +.app__budget .table { + table-layout: fixed; +} + +.app__budget .table__item { + font-weight: 600; + text-align: center; +} + +.app__budget .table__body { + font-size: 1.3rem; +} + +.app__history { + max-width: 100%; + overflow-x: auto; +} + +.app__history .app__title { + position: sticky; + left: 0; +} + +/* + * Components + */ + +.btn { + border-radius: 5px; + font-weight: 600; + margin: auto; + padding: 0.6rem 0.8rem; + transition: all 0.35s ease-in-out 0s; +} + +.btn:hover, +.btn:focus { + transform: scaleX(1.2) scaleY(1.1); +} + +.btn:focus { + outline: none; +} + +.btn:active { + transform: scale(0.95); +} + +.btn--register { + background: hsl(202, 67%, 84%); + border: 2px solid hsl(202, 67%, 34%); + box-shadow: 2px 2px 0 0 hsl(202, 67%, 24%); +} + +.btn--add, +.btn--update { + background: hsl(120, 33%, 84%); + border: 2px solid hsl(120, 33%, 34%); + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%); +} + +.btn--add:focus, +.btn--update:focus { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(120, 33%, 81%); +} + +.btn--add:active, +.btn--update:active { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%); +} + +.btn--delete, +.btn--reset { + background: hsl(0, 33%, 84%); + border: 2px solid hsl(0, 33%, 34%); + box-shadow: 2px 2px 0 0 hsl(0, 33%, 24%); +} + +.btn--delete:focus, +.btn--reset:focus { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(0, 33%, 81%); +} + +.btn--delete:active, +.btn--reset:active { + box-shadow: 2px 2px 0 0 hsl(0, 33%, 24%); +} + +.btn--rename, +.btn--edit { + background: hsl(193, 33%, 84%); + border: 2px solid hsl(193, 33%, 34%); + box-shadow: 2px 2px 0 0 hsl(193, 33%, 24%); +} + +.btn--rename:focus, +.btn--edit:focus { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(193, 33%, 81%); +} + +.btn--delete:active, +.btn--edit:active { + box-shadow: 2px 2px 0 0 hsl(193, 33%, 24%); +} + +.notification { + background: #fff; + border: 3px solid hsl(207, 85%, 27%); + border-radius: 3px; + box-shadow: 1px 1px 3px 0 hsla(0, 0%, 0%, 0.7); + font-weight: 600; + padding: 1rem; +} + +.notification--error { + color: hsl(0, 49%, 39%); + border-color: hsl(0, 49%, 39%); +} + +.notification--info { + color: hsl(212, 76%, 38%); + border-color: hsl(212, 76%, 38%); +} + +.notification--warning { + color: hsl(32, 76%, 38%); + border-color: hsl(32, 76%, 38%); +} + +/* + * Media Queries + */ +@media screen and (min-width: 1200px) { + .main { + margin: auto; + max-width: 1200px; + } + .app__management { + grid-template-columns: repeat(3, min(calc(100vw - 2 * 1rem), 21rem)); + } + + .app__budget { + position: sticky; + top: 1rem; + height: max-content; + } +} 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); +} diff --git a/public/projects/js-small-apps/clock/README.md b/public/projects/js-small-apps/clock/README.md new file mode 100644 index 0000000..7a89cc1 --- /dev/null +++ b/public/projects/js-small-apps/clock/README.md @@ -0,0 +1,19 @@ +# Clock + +What time is it? + +## Description + +You can see the current time in three formats: + +- analog clock +- digital clock +- text based + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#clock + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/clock/app.js b/public/projects/js-small-apps/clock/app.js new file mode 100644 index 0000000..7ae2702 --- /dev/null +++ b/public/projects/js-small-apps/clock/app.js @@ -0,0 +1,135 @@ +function setDate(day, month, year) { + const div = document.getElementById("date"); + div.textContent = `${day}/${month}/${year}`; +} + +function get12Rotation(int) { + const hour = int > 12 ? int - 12 : int; + return (360 / 12) * hour; +} + +function get60Rotation(int) { + return (360 / 60) * int; +} + +function setSvgClockHours(hours) { + const clockHours = document.getElementById("svg-clock_hours"); + clockHours.style.transform = `rotate(${get12Rotation(hours)}deg)`; + clockHours.style.transformOrigin = "center"; +} + +function setSvgClockMinutes(minutes) { + const clockMinutes = document.getElementById("svg-clock_minutes"); + clockMinutes.style.transform = `rotate(${get60Rotation(minutes)}deg)`; + clockMinutes.style.transformOrigin = "center"; +} + +function setSvgClockSeconds(seconds) { + const clockSeconds = document.getElementById("svg-clock_seconds"); + clockSeconds.style.transform = `rotate(${get60Rotation(seconds)}deg)`; + clockSeconds.style.transformOrigin = "center"; +} + +function setDigitalClockHours(hours) { + const clockHours = document.getElementById("digital-clock_hours"); + clockHours.textContent = hours; +} + +function setDigitalClockMinutes(minutes) { + const formatted = minutes < 10 ? `0` + minutes : minutes; + const clockMinutes = document.getElementById("digital-clock_minutes"); + clockMinutes.textContent = formatted; +} + +function getHoursToString(hours) { + const hoursToText = [ + "noon", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + ]; + + return hoursToText[hours % 12]; +} + +function getMinutesToString(minutes) { + const ones = [ + "", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + ]; + const teens = [ + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen", + ]; + const tens = ["", "", "twenty", "thirty", "forty", "fifty"]; + const minutesToArray = minutes.toString().split(""); + let text = ""; + + if (minutes < 10) { + text = ones[minutes]; + } else if (minutes < 20) { + text = teens[minutesToArray[1]]; + } else { + text = `${tens[minutesToArray[0]]} ${ones[minutesToArray[1]]}`; + } + + return text; +} + +function setTextClock(hours, minutes) { + const div = document.getElementById("text-clock"); + const meridiem = hours < 12 ? "am" : "pm"; + div.textContent = `It's ${getHoursToString(hours)} ${getMinutesToString( + minutes + )} ${meridiem}.`; +} + +function updateAll() { + const now = new Date(); + const [month, day, year] = [ + now.getMonth() + 1, + now.getDate(), + now.getFullYear(), + ]; + const [hours, minutes, seconds] = [ + now.getHours(), + now.getMinutes(), + now.getSeconds(), + ]; + + setDate(day, month, year); + setSvgClockHours(hours); + setSvgClockMinutes(minutes); + setSvgClockSeconds(seconds); + setDigitalClockHours(hours); + setDigitalClockMinutes(minutes); + setTextClock(hours, minutes); +} + +updateAll(); +setInterval(updateAll, 1000); diff --git a/public/projects/js-small-apps/clock/index.html b/public/projects/js-small-apps/clock/index.html new file mode 100644 index 0000000..9174170 --- /dev/null +++ b/public/projects/js-small-apps/clock/index.html @@ -0,0 +1,72 @@ +<!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>What time is it?</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="header"> + <h1>What time is it?</h1> + </header> + <main class="app"> + <div id="date"></div> + <svg + id="svg-clock" + viewBox="0 0 135.46667 135.46667" + xmlns="http://www.w3.org/2000/svg" + > + <path + id="path846" + fill="#ffffff" + stroke="#000000" + stroke-width="2.5" + d="M 134.20638,67.73333 A 66.473045,66.473045 0 0 1 67.73333,134.20638 66.473045,66.473045 0 0 1 1.2602844,67.73333 66.473045,66.473045 0 0 1 67.73333,1.2602844 66.473045,66.473045 0 0 1 134.20638,67.73333 Z" + /> + <path + d="m 71.242395,67.733337 a 3.509058,3.509058 0 0 1 -3.509058,3.509058 3.509058,3.509058 0 0 1 -3.509058,-3.509058 3.509058,3.509058 0 0 1 3.509058,-3.509058 3.509058,3.509058 0 0 1 3.509058,3.509058 z" + /> + <path + id="svg-clock_seconds" + d="M 67.265175,4.7740932 H 68.20149 V 68.316688 h -0.936315 z" + /> + <path + id="svg-clock_minutes" + d="m 66.797363,16.601835 h 1.871941 V 68.31669 h -1.871941 z" + /> + <path + id="svg-clock_hours" + d="m 65.857079,29.393064 h 3.752515 V 68.31669 h -3.752515 z" + /> + <path + id="path1522-7" + d="m 67.733338,127.0842 -3.736107,8.23531 h 7.472208 z" + /> + <path + id="path1522-7-3" + d="M 8.3966895,67.733338 0.16137954,63.997231 v 7.472208 z" + /> + <path + id="path1522-7-3-5" + d="m 127.04505,67.733338 8.23531,-3.736107 v 7.472208 z" + /> + <path + id="path1522-7-5" + d="m 67.73333,8.4010979 -3.7361,-8.23531003 h 7.47221 z" + /> + </svg> + <div id="digital-clock"> + <div id="digital-clock_hours" class="digital-clock__value">00</div> + <div class="digital-clock__sep">:</div> + <div id="digital-clock_minutes" class="digital-clock__value">00</div> + </div> + <div id="text-clock"></div> + </main> + <footer class="footer"> + What time is it?. MIT 2021. Armand Philippot. + </footer> + <script src="app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/clock/style.css b/public/projects/js-small-apps/clock/style.css new file mode 100644 index 0000000..f1327e1 --- /dev/null +++ b/public/projects/js-small-apps/clock/style.css @@ -0,0 +1,75 @@ +*, +*::before, +*::after { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +body { + display: flex; + flex-flow: column nowrap; + min-height: 100vh; + font-size: 16px; + font-size: 1rem; + font-family: Georgia, "Times New Roman", Times, serif; +} + +.header, +.app, +.footer { + width: min(calc(100vw - 2rem), 800px); + margin: auto; +} + +.header { + font-size: 2rem; + text-align: center; + margin-bottom: 2rem; + padding: 2rem 0; +} + +.footer { + font-size: 0.9rem; + text-align: center; + margin-top: 3rem; + padding: 2rem 0; +} + +.app { + flex: 1; + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + gap: clamp(1rem, 5vw, 3rem); +} + +#date { + flex: 0 0 100%; + font-size: 1.5rem; + text-align: center; + padding: 1rem 0; +} + +#svg-clock { + width: 20rem; + height: 20rem; +} + +#digital-clock { + display: flex; + flex-flow: row nowrap; + gap: 0.5rem; + width: max-content; + padding: 2rem; + border: 5px solid #000; + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-size: 2rem; + font-weight: 600; +} + +#text-clock { + flex: 0 0 100%; + text-align: center; +} diff --git a/public/projects/js-small-apps/color-cycle/README.md b/public/projects/js-small-apps/color-cycle/README.md new file mode 100644 index 0000000..e9d5970 --- /dev/null +++ b/public/projects/js-small-apps/color-cycle/README.md @@ -0,0 +1,13 @@ +# Color Cycle + +An app to cycle a color value through incremental changes. In other words: play with hexadecimal colors! + +You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Color-Cycle-App.md + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#color-cycle + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/color-cycle/app.js b/public/projects/js-small-apps/color-cycle/app.js new file mode 100644 index 0000000..2ed8e7f --- /dev/null +++ b/public/projects/js-small-apps/color-cycle/app.js @@ -0,0 +1,208 @@ +let isRunning = false; +let intervalId; + +/** + * Check if the provided value is a valid hexadecimal. + * @param {String} value - Two hexadecimal symbols. + * @returns {Boolean} True if value is a valid hexadecimal ; false otherwise. + */ +function isValidHex(value) { + const hexSymbols = "0123456789ABCDEF"; + const hexArray = value.split(""); + if (hexArray.length !== 2) return false; + if (!hexSymbols.includes(hexArray[0].toUpperCase())) return false; + if (!hexSymbols.includes(hexArray[1].toUpperCase())) return false; + return true; +} + +/** + * Print a message to notify user. + * @param {String} msg A message to print. + */ +function notify(msg) { + const body = document.querySelector(".body"); + const notification = document.createElement("div"); + notification.classList.add("notification"); + notification.textContent = msg; + body.appendChild(notification); + + setTimeout(() => { + notification.remove(); + }, 3000); +} + +/** + * Check if colors are set and are valid hexadecimal. + * @param {String} red - Hexadecimal string for red color. + * @param {String} green - Hexadecimal string for green color. + * @param {String} blue - Hexadecimal string for blue color. + * @returns {Boolean} True if colors are ready; false otherwise. + */ +function areColorsReady(red, green, blue) { + if (!red || !green || !blue) return false; + + if (!isValidHex(red)) { + notify("Red is not hexadecimal."); + return false; + } + + if (!isValidHex(green)) { + notify("Green is not hexadecimal."); + return false; + } + + if (!isValidHex(blue)) { + notify("Blue is not hexadecimal."); + return false; + } + + return true; +} + +/** + * Update the preview color with the provided color. + * @param {String} color - Hexadecimal string with a leading hash (CSS format). + */ +function updatePreviewColor(color) { + const preview = document.querySelector(".preview"); + preview.style.backgroundColor = color; +} + +/** + * Initialize the preview with user settings. + * @param {Object} ui - The different element corresponding to the user interface. + */ +function setPreview(ui) { + const red = ui.colors.red.value; + const green = ui.colors.green.value; + const blue = ui.colors.blue.value; + + if (areColorsReady(red, green, blue)) { + const currentColor = `#${red}${green}${blue}`; + updatePreviewColor(currentColor); + } +} + +/** + * Generate a new color. + * @param {Integer} color - An integer corresponding to a RGB value. + * @param {Integer} increment - Add this value to color argument. + */ +function* getColor(color, increment) { + let nextColor = color; + yield nextColor; + + while (true) { + if (nextColor + increment > 255) { + nextColor = nextColor - 255 + increment; + } else { + nextColor = nextColor + increment; + } + + yield nextColor; + } +} + +/** + * Convert an RGB color to hexadecimal format. + * @param {Integer} red - An integer representing red color with RGB format. + * @param {Integer} green - An integer representing green color with RGB format. + * @param {Integer} blue - A value representing blue color with RGB format. + * @returns {String} The color in hexadecimal format with a leading hash. + */ +function getNexHexColor(red, green, blue) { + return `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}`; +} + +/** + * Disable or enable inputs. + * @param {Object} ui - The HTMLElements corresponding to user interface. + */ +function toggleInputs(ui) { + ui.colors.red.disabled = isRunning; + ui.colors.green.disabled = isRunning; + ui.colors.blue.disabled = isRunning; + ui.increments.red.disabled = isRunning; + ui.increments.green.disabled = isRunning; + ui.increments.blue.disabled = isRunning; + ui.interval.disabled = isRunning; +} + +/** + * Start or stop the preview. + * @param {Object} ui - The HTMLElements corresponding to user interface. + */ +function start(ui) { + const red = ui.colors.red.value; + const green = ui.colors.green.value; + const blue = ui.colors.blue.value; + + if (areColorsReady(red, green, blue)) { + isRunning = !isRunning; + ui.button.textContent = isRunning ? "Stop" : "Start"; + } else { + notify("Colors are not correctly set."); + } + + const redIncrement = Number(ui.increments.red.value); + const greenIncrement = Number(ui.increments.green.value); + const blueIncrement = Number(ui.increments.blue.value); + const redGenerator = getColor(parseInt(red, 16), redIncrement); + const greenGenerator = getColor(parseInt(green, 16), greenIncrement); + const blueGenerator = getColor(parseInt(blue, 16), blueIncrement); + const timing = ui.interval.value; + + if (isRunning) { + toggleInputs(ui); + intervalId = setInterval(() => { + const nextRed = redGenerator.next().value; + const nextGreen = greenGenerator.next().value; + const nextBlue = blueGenerator.next().value; + const newColor = getNexHexColor(nextRed, nextGreen, nextBlue); + updatePreviewColor(newColor); + }, timing); + } else { + toggleInputs(ui); + clearInterval(intervalId); + } +} + +/** + * Listen form, buttons and inputs. + * @param {Object} ui - The HTMLElements corresponding to user interface. + */ +function listen(ui) { + ui.form.addEventListener("submit", (e) => { + e.preventDefault(); + }); + ui.button.addEventListener("click", () => start(ui)); + ui.colors.red.addEventListener("change", () => setPreview(ui)); + ui.colors.green.addEventListener("change", () => setPreview(ui)); + ui.colors.blue.addEventListener("change", () => setPreview(ui)); +} + +/** + * Initialize the app. + */ +function init() { + const ui = { + button: document.querySelector(".btn"), + form: document.querySelector(".form"), + colors: { + red: document.getElementById("color-red"), + green: document.getElementById("color-green"), + blue: document.getElementById("color-blue"), + }, + increments: { + red: document.getElementById("increment-red"), + green: document.getElementById("increment-green"), + blue: document.getElementById("increment-blue"), + }, + interval: document.getElementById("time-interval"), + }; + + setPreview(ui); + listen(ui); +} + +init(); diff --git a/public/projects/js-small-apps/color-cycle/index.html b/public/projects/js-small-apps/color-cycle/index.html new file mode 100644 index 0000000..bb99787 --- /dev/null +++ b/public/projects/js-small-apps/color-cycle/index.html @@ -0,0 +1,117 @@ +<!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>Color Cycle</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body class="body"> + <header class="header"> + <h1 class="branding">Color Cycle</h1> + </header> + <main class="main"> + <div class="color-cycle"> + <div class="color-cycle__preview"> + <div class="preview"></div> + <button class="btn" type="button">Start</button> + </div> + <form action="" class="color-cycle__settings form"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Hex Color</legend> + <div class="form__item"> + <label for="color-red" class="form__label">Red</label + ><input + type="text" + name="color-red" + id="color-red" + class="form__input" + size="2" + value="FF" + /> + </div> + <div class="form__item"> + <label for="color-green" class="form__label">Green</label + ><input + type="text" + name="color-green" + id="color-green" + class="form__input" + size="2" + value="FF" + /> + </div> + <div class="form__item"> + <label for="color-blue" class="form__label">Blue</label + ><input + type="text" + name="color-blue" + id="color-blue" + class="form__input" + size="2" + value="FF" + /> + </div> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Increment</legend> + <div class="form__item"> + <label for="increment-red" class="form__label">Red</label + ><input + type="number" + name="increment-red" + id="increment-red" + class="form__input" + size="2" + value="0" + /> + </div> + <div class="form__item"> + <label for="increment-green" class="form__label">Green</label + ><input + type="number" + name="increment-green" + id="increment-green" + class="form__input" + size="2" + value="0" + /> + </div> + <div class="form__item"> + <label for="increment-blue" class="form__label">Blue</label + ><input + type="number" + name="increment-blue" + id="increment-blue" + class="form__input" + size="2" + value="0" + /> + </div> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Time</legend> + <div class="form__item"> + <label for="time-interval" class="form__label" + >Interval (ms)</label + > + <input + type="range" + name="time-interval" + id="time-interval" + min="1" + max="1000" + value="250" + /> + </div> + </fieldset> + </form> + </div> + </main> + <footer class="footer"> + <p class="copyright">Color Cycle. MIT 2021. Armand Philippot.</p> + </footer> + <script src="app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/color-cycle/style.css b/public/projects/js-small-apps/color-cycle/style.css new file mode 100644 index 0000000..62e0e47 --- /dev/null +++ b/public/projects/js-small-apps/color-cycle/style.css @@ -0,0 +1,132 @@ +*, +*::after, +*::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #fff; + color: #000; + 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; +} + +.main { + flex: 1; + margin: 3rem; + position: relative; +} + +.header, +.main, +.footer { + width: min(calc(100vw - 2rem), 80ch); + margin-left: auto; + margin-right: auto; +} + +.header, +.footer { + padding: 1rem 0; +} + +.branding { + color: hsl(219, 64%, 35%); + text-align: center; +} + +.copyright { + font-size: 0.9rem; + text-align: center; +} + +.color-cycle { + display: flex; + flex-flow: row wrap; + gap: 2rem; + align-items: center; +} + +.color-cycle__preview, +.color-cycle__settings { + flex: 1 1 min(calc(100vw - 2rem), calc(80ch / 2 - 1rem)); +} + +.preview { + border: 1px solid #ccc; + height: 20rem; + margin-bottom: 2rem; +} + +.form__fieldset { + display: flex; + flex-flow: row wrap; + gap: 1rem; + margin: 1rem 0; + padding: 0.8rem 1rem 1.2rem; +} + +.form__legend { + color: hsl(219, 64%, 35%); + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + padding: 0 0.8rem; +} + +.form__label { + display: block; + cursor: pointer; +} + +.form__input { + padding: 0.2rem 0.5rem; +} + +.btn, +.form__input { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.btn { + background: #fff; + border: 2px solid hsl(219, 64%, 35%); + border-radius: 5px; + color: hsl(219, 64%, 35%); + font-weight: 600; + cursor: pointer; + display: block; + margin: auto; + padding: 0.5rem 1rem; +} + +.btn:hover, +.btn:focus { + background: hsl(219, 64%, 35%); + color: #fff; +} + +.btn:active { + background: hsl(219, 64%, 25%); + color: #fff; +} + +.notification { + background: #fff; + border: 2px solid hsl(0, 64%, 35%); + color: hsl(0, 64%, 35%); + font-weight: 600; + padding: 1rem; + position: absolute; + right: 1rem; + bottom: 1rem; +} diff --git a/public/projects/js-small-apps/commitlint.config.js b/public/projects/js-small-apps/commitlint.config.js new file mode 100644 index 0000000..28fe5c5 --- /dev/null +++ b/public/projects/js-small-apps/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {extends: ['@commitlint/config-conventional']} diff --git a/public/projects/js-small-apps/css-border-previewer/README.md b/public/projects/js-small-apps/css-border-previewer/README.md new file mode 100644 index 0000000..24a4566 --- /dev/null +++ b/public/projects/js-small-apps/css-border-previewer/README.md @@ -0,0 +1,13 @@ +# CSS Border Previewer + +An app to preview how a shapes looks when you change CSS border properties. + +You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Border-Radius-Previewer.md but I have decided to not limit myself to border-radius. + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#css-border-previewer + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/css-border-previewer/app.js b/public/projects/js-small-apps/css-border-previewer/app.js new file mode 100644 index 0000000..583703d --- /dev/null +++ b/public/projects/js-small-apps/css-border-previewer/app.js @@ -0,0 +1,847 @@ +/** + * Retrieve the border color property name depending on direction. + * @param {String} direction - Either `top`, `right`, `left` or `bottom`. + * @returns {String} The CSS property name. + */ +function getBorderColorProperty(direction) { + let borderColorProperty; + + switch (direction) { + case "top": + borderColorProperty = "borderTopColor"; + break; + case "right": + borderColorProperty = "borderRightColor"; + break; + case "bottom": + borderColorProperty = "borderBottomColor"; + break; + case "left": + borderColorProperty = "borderLeftColor"; + break; + default: + borderColorProperty = "borderColor"; + break; + } + + return borderColorProperty; +} + +/** + * Retrieve the border style property name depending on direction. + * @param {String} direction - Either `top`, `right`, `bottom` or `left`. + * @returns {String} The CSS property name. + */ +function getBorderStyleProperty(direction) { + let borderStyleProperty; + + switch (direction) { + case "top": + borderStyleProperty = "borderTopStyle"; + break; + case "right": + borderStyleProperty = "borderRightStyle"; + break; + case "bottom": + borderStyleProperty = "borderBottomStyle"; + break; + case "left": + borderStyleProperty = "borderLeftStyle"; + break; + default: + borderStyleProperty = "borderStyle"; + break; + } + + return borderStyleProperty; +} + +/** + * Retrieve the border width property name depending on direction. + * @param {String} direction - Either `top`, `right`, `left` or `bottom`. + * @returns {String} The CSS property name. + */ +function getBorderWidthProperty(direction) { + let borderWidthProperty; + + switch (direction) { + case "top": + borderWidthProperty = "borderTopWidth"; + break; + case "right": + borderWidthProperty = "borderRightWidth"; + break; + case "bottom": + borderWidthProperty = "borderBottomWidth"; + break; + case "left": + borderWidthProperty = "borderLeftWidth"; + break; + default: + borderWidthProperty = "borderWidth"; + break; + } + + return borderWidthProperty; +} + +/** + * Apply the custom border to an element. + * @param {HTMLElement} el - Apply border to this element. + * @param {String} property - Either `color`, `style` or `width`. + * @param {String} value - The value to apply. + * @param {String} [direction] - Either `top`, `right`, `bottom` or `left`. + */ +function setBorder(el, property, value, direction = null) { + let borderProperty; + + switch (property) { + case "color": + borderProperty = getBorderColorProperty(direction); + break; + case "style": + borderProperty = getBorderStyleProperty(direction); + break; + case "width": + borderProperty = getBorderWidthProperty(direction); + default: + break; + } + + el.style[borderProperty] = value; +} + +/** + * Apply the custom border radius to an element. + * @param {HTMLElement} el - Apply border radius to this element. + * @param {String} firstRadius - The first radius value. + * @param {String} [secondRadius] - The second radius value. + * @param {String} [x] - The horizontal direction: either `right` or `left`. + * @param {String} [y] - The vertical direction: either `top` or `bottom`. + */ +function setBorderRadius(el, firstRadius, secondRadius, x = null, y = null) { + const direction = `${x}-${y}`; + const value = `${firstRadius}${secondRadius ? ` / ${secondRadius}` : ""}`; + let borderRadiusProperty; + + switch (direction) { + case "left-top": + borderRadiusProperty = "borderTopLeftRadius"; + break; + case "right-top": + borderRadiusProperty = "borderTopRightRadius"; + break; + case "left-bottom": + borderRadiusProperty = "borderBottomLeftRadius"; + break; + case "right-bottom": + borderRadiusProperty = "borderBottomRightRadius"; + break; + default: + borderRadiusProperty = "borderRadius"; + break; + } + + el.style[borderRadiusProperty] = value; +} + +/** + * Display the corresponding border settings. + * @param {String} string - Either `common` or `individual`. + */ +function toggleBorderSettingsDisplay(string) { + const allBordersFieldset = document.getElementById("fieldset-borders"); + const topBorderFieldset = document.getElementById("fieldset-border-top"); + const rightBorderFieldset = document.getElementById("fieldset-border-right"); + const bottomBorderFieldset = document.getElementById( + "fieldset-border-bottom" + ); + const leftBorderFieldset = document.getElementById("fieldset-border-left"); + + if (string === "common") { + allBordersFieldset.style.display = ""; + topBorderFieldset.style.display = "none"; + rightBorderFieldset.style.display = "none"; + bottomBorderFieldset.style.display = "none"; + leftBorderFieldset.style.display = "none"; + } else { + allBordersFieldset.style.display = "none"; + topBorderFieldset.style.display = ""; + rightBorderFieldset.style.display = ""; + bottomBorderFieldset.style.display = ""; + leftBorderFieldset.style.display = ""; + } +} + +/** + * Display the corresponding border-radius settings. + * @param {String} string - Either `common` or `individual`. + */ +function toggleBorderRadiusSettingsDisplay(string) { + const allBordersRadiusFieldset = document.getElementById( + "fieldset-borders-radius" + ); + const topLeftBorderRadiusFieldset = document.getElementById( + "fieldset-border-top-left-radius" + ); + const topRightBorderRadiusFieldset = document.getElementById( + "fieldset-border-top-right-radius" + ); + const bottomLeftBorderRadiusFieldset = document.getElementById( + "fieldset-border-bottom-left-radius" + ); + const bottomRightBorderRadiusFieldset = document.getElementById( + "fieldset-border-bottom-right-radius" + ); + + if (string === "common") { + allBordersRadiusFieldset.style.display = ""; + topLeftBorderRadiusFieldset.style.display = "none"; + topRightBorderRadiusFieldset.style.display = "none"; + bottomLeftBorderRadiusFieldset.style.display = "none"; + bottomRightBorderRadiusFieldset.style.display = "none"; + } else { + allBordersRadiusFieldset.style.display = "none"; + topLeftBorderRadiusFieldset.style.display = ""; + topRightBorderRadiusFieldset.style.display = ""; + bottomLeftBorderRadiusFieldset.style.display = ""; + bottomRightBorderRadiusFieldset.style.display = ""; + } +} + +/** + * Print the generated code into the given element. + * @param {HTMLElement} el - The element where to print generated code. + */ +function printCode(el) { + const code = document.querySelector(".result__code"); + let codeOutput = ` +.box {\n`; + + for (const property of el.style) { + codeOutput += `\t${property}: ${el.style[property]};\n`; + } + + codeOutput += "}"; + code.textContent = codeOutput; +} + +/** + * Check which type of settings is checked. + * @param {String} radioValue - The input radio value. + * @returns {Boolean} True if is individual; false if is common. + */ +function isIndividualSettings(radioValue) { + return radioValue === "true" ? true : false; +} + +/** + * Set all borders to a given element. + * @param {HTMLElement} el - Apply border to this element. + */ +function setCommonBorder(el) { + const allBordersColorInput = document.getElementById("borders-color"); + const allBordersStyleSelect = document.getElementById("borders-style"); + const allBordersUnitSelect = document.getElementById("borders-unit"); + const allBordersWidthInput = document.getElementById("borders-width"); + + setBorder(el, "color", allBordersColorInput.value); + setBorder(el, "style", allBordersStyleSelect.value); + setBorder( + el, + "width", + `${allBordersWidthInput.value}${allBordersUnitSelect.value}` + ); + + allBordersColorInput.addEventListener("input", () => { + setBorder(el, "color", allBordersColorInput.value); + printCode(el); + }); + + allBordersStyleSelect.addEventListener("input", () => { + setBorder(el, "style", allBordersStyleSelect.value); + printCode(el); + }); + + allBordersUnitSelect.addEventListener("input", () => { + setBorder( + el, + "width", + `${allBordersWidthInput.value}${allBordersUnitSelect.value}` + ); + printCode(el); + }); + + allBordersWidthInput.addEventListener("input", () => { + setBorder( + el, + "width", + `${allBordersWidthInput.value}${allBordersUnitSelect.value}` + ); + printCode(el); + }); +} + +/** + * Set the top border to the given element. + * @param {HTMLElement} el - Apply the top border to this element. + */ +function setTopBorder(el) { + const topBorderColorInput = document.getElementById("border-top-color"); + const topBorderStyleSelect = document.getElementById("border-top-style"); + const topBorderUnitSelect = document.getElementById("border-top-unit"); + const topBorderWidthInput = document.getElementById("border-top-width"); + + setBorder(el, "color", topBorderColorInput.value, "top"); + setBorder(el, "style", topBorderStyleSelect.value, "top"); + setBorder( + el, + "width", + `${topBorderWidthInput.value}${topBorderUnitSelect.value}`, + "top" + ); + + topBorderColorInput.addEventListener("input", () => { + setBorder(el, "color", topBorderColorInput.value, "top"); + printCode(el); + }); + + topBorderStyleSelect.addEventListener("input", () => { + setBorder(el, "style", topBorderStyleSelect.value, "top"); + printCode(el); + }); + + topBorderUnitSelect.addEventListener("input", () => { + setBorder( + el, + "width", + `${topBorderWidthInput.value}${topBorderUnitSelect.value}`, + "top" + ); + printCode(el); + }); + + topBorderWidthInput.addEventListener("input", () => { + setBorder( + el, + "width", + `${topBorderWidthInput.value}${topBorderUnitSelect.value}`, + "top" + ); + printCode(el); + }); +} + +/** + * Set the right border to the given element. + * @param {HTMLElement} el - Apply the right border to this element. + */ +function setRightBorder(el) { + const rightBorderWidthInput = document.getElementById("border-right-width"); + const rightBorderUnitSelect = document.getElementById("border-right-unit"); + const rightBorderStyleSelect = document.getElementById("border-right-style"); + const rightBorderColorInput = document.getElementById("border-right-color"); + + setBorder(el, "color", rightBorderColorInput.value, "right"); + setBorder(el, "style", rightBorderStyleSelect.value, "right"); + setBorder( + el, + "width", + `${rightBorderWidthInput.value}${rightBorderUnitSelect.value}`, + "right" + ); + + rightBorderColorInput.addEventListener("input", () => { + setBorder(el, "color", rightBorderColorInput.value, "right"); + printCode(el); + }); + + rightBorderStyleSelect.addEventListener("input", () => { + setBorder(el, "style", rightBorderStyleSelect.value, "right"); + printCode(el); + }); + + rightBorderUnitSelect.addEventListener("input", () => { + setBorder( + el, + "width", + `${rightBorderWidthInput.value}${rightBorderUnitSelect.value}`, + "right" + ); + printCode(el); + }); + + rightBorderWidthInput.addEventListener("input", () => { + setBorder( + el, + "width", + `${rightBorderWidthInput.value}${rightBorderUnitSelect.value}`, + "right" + ); + printCode(el); + }); +} + +/** + * Set the bottom border to the given element. + * @param {HTMLElement} el - Apply the bottom border to this element. + */ +function setBottomBorder(el) { + const bottomBorderWidthInput = document.getElementById("border-bottom-width"); + const bottomBorderUnitSelect = document.getElementById("border-bottom-unit"); + const bottomBorderStyleSelect = document.getElementById( + "border-bottom-style" + ); + const bottomBorderColorInput = document.getElementById("border-bottom-color"); + + setBorder(el, "color", bottomBorderColorInput.value, "bottom"); + setBorder(el, "style", bottomBorderStyleSelect.value, "bottom"); + setBorder( + el, + "width", + `${bottomBorderWidthInput.value}${bottomBorderUnitSelect.value}`, + "bottom" + ); + + bottomBorderColorInput.addEventListener("input", () => { + setBorder(el, "color", bottomBorderColorInput.value, "bottom"); + printCode(el); + }); + + bottomBorderStyleSelect.addEventListener("input", () => { + setBorder(el, "style", bottomBorderStyleSelect.value, "bottom"); + printCode(el); + }); + + bottomBorderUnitSelect.addEventListener("input", () => { + setBorder( + el, + "width", + `${bottomBorderWidthInput.value}${bottomBorderUnitSelect.value}`, + "bottom" + ); + printCode(el); + }); + + bottomBorderWidthInput.addEventListener("input", () => { + setBorder( + el, + "width", + `${bottomBorderWidthInput.value}${bottomBorderUnitSelect.value}`, + "bottom" + ); + printCode(el); + }); +} + +/** + * Set the left border to the given element. + * @param {HTMLElement} el - Apply the left border to this element. + */ +function setLeftBorder(el) { + const leftBorderWidthInput = document.getElementById("border-left-width"); + const leftBorderUnitSelect = document.getElementById("border-left-unit"); + const leftBorderStyleSelect = document.getElementById("border-left-style"); + const leftBorderColorInput = document.getElementById("border-left-color"); + + setBorder(el, "color", leftBorderColorInput.value, "left"); + setBorder(el, "style", leftBorderStyleSelect.value, "left"); + setBorder( + el, + "width", + `${leftBorderWidthInput.value}${leftBorderUnitSelect.value}`, + "left" + ); + + leftBorderColorInput.addEventListener("input", () => { + setBorder(el, "color", leftBorderColorInput.value, "left"); + printCode(el); + }); + + leftBorderStyleSelect.addEventListener("input", () => { + setBorder(el, "style", leftBorderStyleSelect.value, "left"); + printCode(el); + }); + + leftBorderUnitSelect.addEventListener("input", () => { + setBorder( + el, + "width", + `${leftBorderWidthInput.value}${leftBorderUnitSelect.value}`, + "left" + ); + printCode(el); + }); + + leftBorderWidthInput.addEventListener("input", () => { + setBorder( + el, + "width", + `${leftBorderWidthInput.value}${leftBorderUnitSelect.value}`, + "left" + ); + printCode(el); + }); +} + +/** + * Set all borders radius to the given element. + * @param {HTMLElement} el - Apply the border radius to this element. + */ +function setCommonBorderRadius(el) { + const borderCommonFirstRadius = document.getElementById( + "borders-first-radius" + ); + const borderCommonFirstRadiusUnit = document.getElementById( + "borders-first-radius-unit" + ); + const borderCommonSecondRadius = document.getElementById( + "borders-second-radius" + ); + const borderCommonSecondRadiusUnit = document.getElementById( + "borders-second-radius-unit" + ); + let firstRadius = `${borderCommonFirstRadius.value}${borderCommonFirstRadiusUnit.value}`; + let secondRadius = borderCommonSecondRadius.value + ? `${borderCommonSecondRadius.value}${borderCommonSecondRadiusUnit.value}` + : null; + + setBorderRadius(el, firstRadius, secondRadius); + + borderCommonFirstRadius.addEventListener("input", () => { + firstRadius = `${borderCommonFirstRadius.value}${borderCommonFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius); + printCode(el); + }); + + borderCommonFirstRadiusUnit.addEventListener("input", () => { + firstRadius = `${borderCommonFirstRadius.value}${borderCommonFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius); + printCode(el); + }); + + borderCommonSecondRadius.addEventListener("input", () => { + secondRadius = borderCommonSecondRadius.value + ? `${borderCommonSecondRadius.value}${borderCommonSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius); + printCode(el); + }); + + borderCommonSecondRadiusUnit.addEventListener("input", () => { + secondRadius = borderCommonSecondRadius.value + ? `${borderCommonSecondRadius.value}${borderCommonSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius); + printCode(el); + }); +} + +/** + * Set the top left border-radius to the given element. + * @param {HTMLElement} el - Apply the top left border-radius to this element. + */ +function setTopLeftBorderRadius(el) { + const borderTopLeftFirstRadius = document.getElementById( + "border-top-left-first-radius" + ); + const borderTopLeftFirstRadiusUnit = document.getElementById( + "border-top-left-first-radius-unit" + ); + const borderTopLeftSecondRadius = document.getElementById( + "border-top-left-second-radius" + ); + const borderTopLeftSecondRadiusUnit = document.getElementById( + "border-top-left-second-radius-unit" + ); + let firstRadius = `${borderTopLeftFirstRadius.value}${borderTopLeftFirstRadiusUnit.value}`; + let secondRadius = borderTopLeftSecondRadius.value + ? `${borderTopLeftSecondRadius.value}${borderTopLeftSecondRadiusUnit.value}` + : null; + + setBorderRadius(el, firstRadius, secondRadius, "left", "top"); + + borderTopLeftFirstRadius.addEventListener("input", () => { + firstRadius = `${borderTopLeftFirstRadius.value}${borderTopLeftFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "left", "top"); + printCode(el); + }); + + borderTopLeftFirstRadiusUnit.addEventListener("input", () => { + firstRadius = `${borderTopLeftFirstRadius.value}${borderTopLeftFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "left", "top"); + printCode(el); + }); + + borderTopLeftSecondRadius.addEventListener("input", () => { + secondRadius = borderTopLeftSecondRadius.value + ? `${borderTopLeftSecondRadius.value}${borderTopLeftSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "left", "top"); + printCode(el); + }); + + borderTopLeftSecondRadiusUnit.addEventListener("input", () => { + secondRadius = borderTopLeftSecondRadius.value + ? `${borderTopLeftSecondRadius.value}${borderTopLeftSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "left", "top"); + printCode(el); + }); +} + +/** + * Set the top right border-radius to the given element. + * @param {HTMLElement} el - Apply the top right border-radius to this element. + */ +function setTopRightBorderRadius(el) { + const borderTopRightFirstRadius = document.getElementById( + "border-top-right-first-radius" + ); + const borderTopRightFirstRadiusUnit = document.getElementById( + "border-top-right-first-radius-unit" + ); + const borderTopRightSecondRadius = document.getElementById( + "border-top-right-second-radius" + ); + const borderTopRightSecondRadiusUnit = document.getElementById( + "border-top-right-second-radius-unit" + ); + const firstRadius = `${borderTopRightFirstRadius.value}${borderTopRightFirstRadiusUnit.value}`; + const secondRadius = borderTopRightSecondRadius.value + ? `${borderTopRightSecondRadius.value}${borderTopRightSecondRadiusUnit.value}` + : null; + + setBorderRadius(el, firstRadius, secondRadius, "right", "top"); + + borderTopRightFirstRadius.addEventListener("input", () => { + firstRadius = `${borderTopRightFirstRadius.value}${borderTopRightFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "right", "top"); + printCode(el); + }); + + borderTopRightFirstRadiusUnit.addEventListener("input", () => { + firstRadius = `${borderTopRightFirstRadius.value}${borderTopRightFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "right", "top"); + printCode(el); + }); + + borderTopRightSecondRadius.addEventListener("input", () => { + secondRadius = borderTopRightSecondRadius.value + ? `${borderTopRightSecondRadius.value}${borderTopRightSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "right", "top"); + printCode(el); + }); + + borderTopRightSecondRadiusUnit.addEventListener("input", () => { + secondRadius = borderTopRightSecondRadius.value + ? `${borderTopRightSecondRadius.value}${borderTopRightSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "right", "top"); + printCode(el); + }); +} + +/** + * Set the bottom left border-radius to the given element. + * @param {HTMLElement} el - Apply bottom left border-radius to this element. + */ +function setBottomLeftBorderRadius(el) { + const borderBottomLeftFirstRadius = document.getElementById( + "border-bottom-left-first-radius" + ); + const borderBottomLeftFirstRadiusUnit = document.getElementById( + "border-bottom-left-first-radius-unit" + ); + const borderBottomLeftSecondRadius = document.getElementById( + "border-bottom-left-second-radius" + ); + const borderBottomLeftSecondRadiusUnit = document.getElementById( + "border-bottom-left-second-radius-unit" + ); + const firstRadius = `${borderBottomLeftFirstRadius.value}${borderBottomLeftFirstRadiusUnit.value}`; + const secondRadius = borderBottomLeftSecondRadius.value + ? `${borderBottomLeftSecondRadius.value}${borderBottomLeftSecondRadiusUnit.value}` + : null; + + setBorderRadius(el, firstRadius, secondRadius, "left", "bottom"); + + borderBottomLeftFirstRadius.addEventListener("input", () => { + firstRadius = `${borderBottomLeftFirstRadius.value}${borderBottomLeftFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "left", "bottom"); + printCode(el); + }); + + borderBottomLeftFirstRadiusUnit.addEventListener("input", () => { + firstRadius = `${borderBottomLeftFirstRadius.value}${borderBottomLeftFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "left", "bottom"); + printCode(el); + }); + + borderBottomLeftSecondRadius.addEventListener("input", () => { + secondRadius = borderBottomLeftSecondRadius.value + ? `${borderBottomLeftSecondRadius.value}${borderBottomLeftSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "left", "bottom"); + printCode(el); + }); + + borderBottomLeftSecondRadiusUnit.addEventListener("input", () => { + secondRadius = borderBottomLeftSecondRadius.value + ? `${borderBottomLeftSecondRadius.value}${borderBottomLeftSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "left", "bottom"); + printCode(el); + }); +} + +/** + * Set the bottom right border-radius to the given element. + * @param {HTMLElement} el - Apply bottom right border-radius to this element. + */ +function setBottomRightBorderRadius(el) { + const borderBottomRightFirstRadius = document.getElementById( + "border-bottom-right-first-radius" + ); + const borderBottomRightFirstRadiusUnit = document.getElementById( + "border-bottom-right-first-radius-unit" + ); + const borderBottomRightSecondRadius = document.getElementById( + "border-bottom-right-second-radius" + ); + const borderBottomRightSecondRadiusUnit = document.getElementById( + "border-bottom-right-second-radius-unit" + ); + const firstRadius = `${borderBottomRightFirstRadius.value}${borderBottomRightFirstRadiusUnit.value}`; + const secondRadius = borderBottomRightSecondRadius.value + ? `${borderBottomRightSecondRadius.value}${borderBottomRightSecondRadiusUnit.value}` + : null; + + setBorderRadius(el, firstRadius, secondRadius, "right", "bottom"); + + borderBottomRightFirstRadius.addEventListener("input", () => { + firstRadius = `${borderBottomRightFirstRadius.value}${borderBottomRightFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "right", "bottom"); + printCode(el); + }); + + borderBottomRightFirstRadiusUnit.addEventListener("input", () => { + firstRadius = `${borderBottomRightFirstRadius.value}${borderBottomRightFirstRadiusUnit.value}`; + setBorderRadius(el, firstRadius, secondRadius, "right", "bottom"); + printCode(el); + }); + + borderBottomRightSecondRadius.addEventListener("input", () => { + secondRadius = borderBottomRightSecondRadius.value + ? `${borderBottomRightSecondRadius.value}${borderBottomRightSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "right", "bottom"); + printCode(el); + }); + + borderBottomRightSecondRadiusUnit.addEventListener("input", () => { + secondRadius = borderBottomRightSecondRadius.value + ? `${borderBottomRightSecondRadius.value}${borderBottomRightSecondRadiusUnit.value}` + : null; + setBorderRadius(el, firstRadius, secondRadius, "right", "bottom"); + printCode(el); + }); +} + +/** + * Display a message inside the given element. + * @param {HTMLElement} el - The element where to print the message. + * @param {String} msg - The message to display. + * @param {Number} [duration] - The message duration. + */ +function printMessage(el, msg, duration = 1000) { + const backupContent = el.textContent; + + el.textContent = msg; + setTimeout(() => (el.textContent = backupContent), duration); +} + +/** + * Copy code to the clipboard. + */ +function copyCode() { + const code = document.querySelector(".result__code"); + navigator.clipboard.writeText(code.textContent); +} + +/** + * Listen the button copy to clipboard. + */ +function listenCopyCodeBtn() { + const btn = document.getElementById("copy-code"); + + btn.addEventListener("click", () => { + copyCode(); + printMessage(btn, "Copied to clipboard!"); + }); +} + +/** + * Initialize borders settings and borders. + * @param {String} radioValue - The input radio value. + * @param {HTMLElement} el - The element where to apply borders. + */ +function initBorders(radioValue, el) { + if (isIndividualSettings(radioValue)) { + toggleBorderSettingsDisplay("individual"); + setTopBorder(el); + setRightBorder(el); + setBottomBorder(el); + setLeftBorder(el); + } else { + toggleBorderSettingsDisplay("common"); + setCommonBorder(el); + } +} + +/** + * Initialize border-radius settings and border-radius. + * @param {String} radioValue - The input radio value. + * @param {HTMLElement} el - The element where to apply border-radius. + */ +function initBordersRadius(radioValue, el) { + if (isIndividualSettings(radioValue)) { + toggleBorderRadiusSettingsDisplay("individual"); + setTopLeftBorderRadius(el); + setTopRightBorderRadius(el); + setBottomLeftBorderRadius(el); + setBottomRightBorderRadius(el); + } else { + toggleBorderRadiusSettingsDisplay("common"); + setCommonBorderRadius(el); + } +} + +/** + * Initialize the app. + */ +function init() { + const box = document.querySelector(".box"); + const borderPropertyRadio = document.querySelectorAll( + 'input[name="border-property"]' + ); + const borderRadiusPropertyRadio = document.querySelectorAll( + 'input[name="border-radius-property"]' + ); + + for (const radio of borderPropertyRadio) { + if (radio.checked) initBorders(radio.value, box); + radio.addEventListener("change", () => initBorders(radio.value, box)); + } + + for (const radio of borderRadiusPropertyRadio) { + if (radio.checked) initBordersRadius(radio.value, box); + radio.addEventListener("change", () => initBordersRadius(radio.value, box)); + } + + printCode(box); + listenCopyCodeBtn(); +} + +init(); diff --git a/public/projects/js-small-apps/css-border-previewer/index.html b/public/projects/js-small-apps/css-border-previewer/index.html new file mode 100644 index 0000000..bf16fa5 --- /dev/null +++ b/public/projects/js-small-apps/css-border-previewer/index.html @@ -0,0 +1,625 @@ +<!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>CSS Border Previewer</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="header"> + <h1 class="branding">CSS Border Previewer</h1> + </header> + <main class="main"> + <div class="preview"> + <div class="box"></div> + </div> + <form action="#" method="POST" class="settings form"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Global settings</legend> + <div class="form__item"> + Control border property separately? + <input + type="radio" + name="border-property" + id="border-property-no" + value="false" + checked + /> + <label for="border-property-no">No</label> + <input + type="radio" + name="border-property" + id="border-property-yes" + value="true" + /> + <label for="border-property-yes">Yes</label> + </div> + <div class="form__item"> + Control border radius property separately? + <input + type="radio" + name="border-radius-property" + id="border-radius-property-no" + value="false" + checked + /> + <label for="border-radius-property-no">No</label> + <input + type="radio" + name="border-radius-property" + id="border-radius-property-yes" + value="true" + /> + <label for="border-radius-property-yes">Yes</label> + </div> + </fieldset> + <fieldset + id="fieldset-borders" + class="form__fieldset form__fieldset--flex" + > + <legend class="form__legend">Borders settings</legend> + <div class="form__item form__item--flex"> + <label for="borders-width">Width</label> + <input + type="number" + name="borders-width" + id="borders-width" + size="3" + value="1" + class="form__input" + /> + </div> + <div class="form__item form__item--flex"> + <label for="borders-unit" class="form__label">Unit</label> + <select name="borders-unit" id="borders-unit" class="form__select"> + <option value="px" selected>px</option> + <option value="cm">cm</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="borders-style">Style</label> + <select + name="borders-style" + id="borders-style" + class="form__select" + > + <option value="none">None</option> + <option value="dashed">Dashed</option> + <option value="dotted">Dotted</option> + <option value="double">Double</option> + <option value="groove">Groove</option> + <option value="hidden">Hidden</option> + <option value="inset">Inset</option> + <option value="outset">Outset</option> + <option value="ridge">Ridge</option> + <option value="solid" selected>Solid</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="borders-color">Color</label> + <input + type="color" + name="borders-color" + id="borders-color" + value="#000000" + class="form__input form__input--color" + /> + </div> + </fieldset> + <fieldset + id="fieldset-border-top" + class="form__fieldset form__fieldset--flex" + > + <legend class="form__legend">Border-top settings</legend> + <div class="form__item form__item--flex"> + <label for="border-top-width">Width</label> + <input + type="number" + name="border-top-width" + id="border-top-width" + size="3" + value="1" + class="form__input" + /> + </div> + <div class="form__item form__item--flex"> + <label for="border-top-unit" class="form__label">Unit</label> + <select + name="border-top-unit" + id="border-top-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="cm">cm</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-top-style">Style</label> + <select + name="border-top-style" + id="border-top-style" + class="form__select" + > + <option value="none">None</option> + <option value="dashed">Dashed</option> + <option value="dotted">Dotted</option> + <option value="double">Double</option> + <option value="groove">Groove</option> + <option value="hidden">Hidden</option> + <option value="inset">Inset</option> + <option value="outset">Outset</option> + <option value="ridge">Ridge</option> + <option value="solid" selected>Solid</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-top-color">Color</label> + <input + type="color" + name="border-top-color" + id="border-top-color" + value="#000000" + class="form__input form__input--color" + /> + </div> + </fieldset> + <fieldset + id="fieldset-border-right" + class="form__fieldset form__fieldset--flex" + > + <legend class="form__legend">Border-right settings</legend> + <div class="form__item form__item--flex"> + <label for="border-right-width">Width</label> + <input + type="number" + name="border-right-width" + id="border-right-width" + size="3" + value="1" + class="form__input" + /> + </div> + <div class="form__item form__item--flex"> + <label for="border-right-unit" class="form__label">Unit</label> + <select + name="border-right-unit" + id="border-right-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="cm">cm</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-right-style">Style</label> + <select + name="border-right-style" + id="border-right-style" + class="form__select" + > + <option value="none">None</option> + <option value="dashed">Dashed</option> + <option value="dotted">Dotted</option> + <option value="double">Double</option> + <option value="groove">Groove</option> + <option value="hidden">Hidden</option> + <option value="inset">Inset</option> + <option value="outset">Outset</option> + <option value="ridge">Ridge</option> + <option value="solid" selected>Solid</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-right-color">Color</label> + <input + type="color" + name="border-right-color" + id="border-right-color" + value="#000000" + class="form__input form__input--color" + /> + </div> + </fieldset> + <fieldset + id="fieldset-border-bottom" + class="form__fieldset form__fieldset--flex" + > + <legend class="form__legend">Border-bottom settings</legend> + <div class="form__item form__item--flex"> + <label for="border-bottom-width">Width</label> + <input + type="number" + name="border-bottom-width" + id="border-bottom-width" + size="3" + value="1" + class="form__input" + /> + </div> + <div class="form__item form__item--flex"> + <label for="border-bottom-unit" class="form__label">Unit</label> + <select + name="border-bottom-unit" + id="border-bottom-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="cm">cm</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-bottom-style">Style</label> + <select + name="border-bottom-style" + id="border-bottom-style" + class="form__select" + > + <option value="none">None</option> + <option value="dashed">Dashed</option> + <option value="dotted">Dotted</option> + <option value="double">Double</option> + <option value="groove">Groove</option> + <option value="hidden">Hidden</option> + <option value="inset">Inset</option> + <option value="outset">Outset</option> + <option value="ridge">Ridge</option> + <option value="solid" selected>Solid</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-bottom-color">Color</label> + <input + type="color" + name="border-bottom-color" + id="border-bottom-color" + value="#000000" + class="form__input form__input--color" + /> + </div> + </fieldset> + <fieldset + id="fieldset-border-left" + class="form__fieldset form__fieldset--flex" + > + <legend class="form__legend">Border-left settings</legend> + <div class="form__item form__item--flex"> + <label for="border-left-width">Width</label> + <input + type="number" + name="border-left-width" + id="border-left-width" + size="3" + value="1" + class="form__input" + /> + </div> + <div class="form__item form__item--flex"> + <label for="border-left-unit" class="form__label">Unit</label> + <select + name="border-left-unit" + id="border-left-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="cm">cm</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-left-style">Style</label> + <select + name="border-left-style" + id="border-left-style" + class="form__select" + > + <option value="none">None</option> + <option value="dashed">Dashed</option> + <option value="dotted">Dotted</option> + <option value="double">Double</option> + <option value="groove">Groove</option> + <option value="hidden">Hidden</option> + <option value="inset">Inset</option> + <option value="outset">Outset</option> + <option value="ridge">Ridge</option> + <option value="solid" selected>Solid</option> + </select> + </div> + <div class="form__item form__item--flex"> + <label for="border-left-color">Color</label> + <input + type="color" + name="border-left-color" + id="border-left-color" + value="#000000" + class="form__input form__input--color" + /> + </div> + </fieldset> + <fieldset id="fieldset-borders-radius" class="form__fieldset"> + <legend class="form__legend">Border-radius settings</legend> + <div class="form__item"> + <p>First radius:</p> + <label for="borders-first-radius">Value</label> + <input + type="number" + name="borders-first-radius" + id="borders-first-radius" + class="form__input" + /> + <label for="borders-first-radius-unit" class="form__label" + >Unit</label + > + <select + name="borders-first-radius-unit" + id="borders-first-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item"> + <p>Second radius:</p> + <label for="borders-second-radius">Value</label> + <input + type="number" + name="borders-second-radius" + id="borders-second-radius" + class="form__input" + /> + <label for="borders-second-radius-unit" class="form__label" + >Unit</label + > + <select + name="borders-second-radius-unit" + id="borders-second-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + </fieldset> + <fieldset id="fieldset-border-top-left-radius" class="form__fieldset"> + <legend class="form__legend">Border-top-left-radius settings</legend> + <div class="form__item"> + <p>First radius:</p> + <label for="border-top-left-first-radius">Value</label> + <input + type="number" + name="border-top-left-first-radius" + id="border-top-left-first-radius" + class="form__input" + /> + <label for="border-top-left-first-radius-unit" class="form__label" + >Unit</label + > + <select + name="border-top-left-first-radius-unit" + id="border-top-left-first-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item"> + <p>Second radius:</p> + <label for="border-top-left-second-radius">Value</label> + <input + type="number" + name="border-top-left-second-radius" + id="border-top-left-second-radius" + class="form__input" + /> + <label for="border-top-left-second-radius-unit" class="form__label" + >Unit</label + > + <select + name="border-top-left-second-radius-unit" + id="border-top-left-second-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + </fieldset> + <fieldset id="fieldset-border-top-right-radius" class="form__fieldset"> + <legend class="form__legend">Border-top-right-radius settings</legend> + <div class="form__item"> + <p>First radius:</p> + <label for="border-top-right-first-radius">Value</label> + <input + type="number" + name="border-top-right-first-radius" + id="border-top-right-first-radius" + class="form__input" + /> + <label for="border-top-right-first-radius-unit" class="form__label" + >Unit</label + > + <select + name="border-top-right-first-radius-unit" + id="border-top-right-first-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item"> + <p>Second radius:</p> + <label for="border-top-right-second-radius">Value</label> + <input + type="number" + name="border-top-right-second-radius" + id="border-top-right-second-radius" + class="form__input" + /> + <label for="border-top-right-second-radius-unit" class="form__label" + >Unit</label + > + <select + name="border-top-right-second-radius-unit" + id="border-top-right-second-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + </fieldset> + <fieldset + id="fieldset-border-bottom-left-radius" + class="form__fieldset" + > + <legend class="form__legend"> + Border-bottom-left-radius settings + </legend> + <div class="form__item"> + <p>First radius:</p> + <label for="border-bottom-left-first-radius">Value</label> + <input + type="number" + name="border-bottom-left-first-radius" + id="border-bottom-left-first-radius" + class="form__input" + /> + <label + for="border-bottom-left-first-radius-unit" + class="form__label" + >Unit</label + > + <select + name="border-bottom-left-first-radius-unit" + id="border-bottom-left-first-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item"> + <p>Second radius:</p> + <label for="border-bottom-left-second-radius">Value</label> + <input + type="number" + name="border-bottom-left-second-radius" + id="border-bottom-left-second-radius" + class="form__input" + /> + <label + for="border-bottom-left-second-radius-unit" + class="form__label" + >Unit</label + > + <select + name="border-bottom-left-second-radius-unit" + id="border-bottom-left-second-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + </fieldset> + <fieldset + id="fieldset-border-bottom-right-radius" + class="form__fieldset" + > + <legend class="form__legend"> + Border-bottom-right-radius settings + </legend> + <div class="form__item"> + <p>First radius:</p> + <label for="border-bottom-right-first-radius">Value</label> + <input + type="number" + name="border-bottom-right-first-radius" + id="border-bottom-right-first-radius" + class="form__input" + /> + <label + for="border-bottom-right-first-radius-unit" + class="form__label" + >Unit</label + > + <select + name="border-bottom-right-first-radius-unit" + id="border-bottom-right-first-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + <div class="form__item"> + <p>Second radius:</p> + <label for="border-bottom-right-second-radius">Value</label> + <input + type="number" + name="border-bottom-right-second-radius" + id="border-bottom-right-second-radius" + class="form__input" + /> + <label + for="border-bottom-right-second-radius-unit" + class="form__label" + >Unit</label + > + <select + name="border-bottom-right-second-radius-unit" + id="border-bottom-right-second-radius-unit" + class="form__select" + > + <option value="px" selected>px</option> + <option value="%">%</option> + <option value="em">em</option> + <option value="rem">rem</option> + </select> + </div> + </fieldset> + </form> + <div class="result"> + <pre class="result__pre"> + <code class="result__code"></code> + </pre> + <button id="copy-code" class="btn">Copy to the clipboard</button> + </div> + </main> + <footer class="footer"> + <p class="copyright">CSS Border Previewer. MIT 2021. Armand Philippot.</p> + </footer> + <script src="app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/css-border-previewer/style.css b/public/projects/js-small-apps/css-border-previewer/style.css new file mode 100644 index 0000000..458d31c --- /dev/null +++ b/public/projects/js-small-apps/css-border-previewer/style.css @@ -0,0 +1,181 @@ +*, +*::after, +*::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #fff; + color: #000; + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem; + line-height: 1.618; + display: flex; + flex-flow: column nowrap; + min-height: 100vh; +} + +.header, +.footer, +.result { + width: min(calc(100vw - 2rem), calc(1200px - 2rem)); + margin-left: auto; + margin-right: auto; +} + +.preview, +.settings { + width: min(calc(100vw - 2rem), calc(1200px / 2 - 2rem)); + margin-left: auto; + margin-right: auto; +} + +@media screen and (min-width: 1200px) { + .preview { + margin-right: 0; + } + + .settings { + margin-left: 0; + } +} + +.header { + padding: 2rem 0 3rem; + text-align: center; +} + +.branding { + color: hsl(219, 64%, 35%); +} + +.main { + flex: 1; + display: flex; + flex-flow: row wrap; + gap: 1rem; + margin: auto; +} + +.preview { + border: 1px solid #ccc; + padding: 3rem; + position: relative; +} + +.box { + border: 1px solid #000; + width: 100%; + height: 20vh; + position: sticky; + top: 3rem; +} + +.result { + max-width: 100%; +} + +.result__pre { + background: #333; + color: #fff; + min-height: 20rem; + margin: 1rem auto; + padding: 0 1rem; + overflow-x: auto; + tab-size: 4; +} + +.result .btn { + margin: auto; +} + +@media screen and (min-width: 1440px) { + .result__pre { + margin: 1rem 0; + } +} + +.footer { + margin-top: 2rem; + padding: 1rem 0; +} + +.copyright { + font-size: 0.9rem; + text-align: center; +} + +.form p { + font-weight: 600; +} + +.form__fieldset { + border-color: hsl(219, 64%, 35%); + margin-bottom: 1rem; + padding: 0.5rem 1rem 1rem; +} + +.form__fieldset--flex { + display: flex; + flex-flow: row wrap; + gap: 1rem; +} + +.form__legend { + color: hsl(219, 64%, 35%); + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + padding: 0 0.5rem; +} + +.form__item--flex { + display: flex; + flex-flow: column wrap; +} + +.form__item:not(.form__item--flex) + * { + margin-top: 1rem; +} + +.form__input, +.form__select { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.form__input { + padding: 0.2rem 0.5rem; +} + +.form__select { + padding: 0.4rem 0.5rem; +} + +.btn { + background: #fff; + border: 2px solid hsl(219, 64%, 35%); + box-shadow: 1px 1px 2px hsl(219, 64%, 15%), 0 0 2px 1px hsl(219, 64%, 15%); + color: hsl(219, 64%, 35%); + font-family: inherit; + font-size: inherit; + font-weight: 600; + line-height: inherit; + display: block; + padding: 0.5rem; + cursor: pointer; + transition: all 0.15s ease-in-out 0s; +} + +.btn:hover, +.btn:focus { + background: hsl(219, 64%, 35%); + color: #fff; +} + +.btn:active { + transform: translateX(1px) translateY(1px); +} diff --git a/public/projects/js-small-apps/package.json b/public/projects/js-small-apps/package.json new file mode 100644 index 0000000..6f4c753 --- /dev/null +++ b/public/projects/js-small-apps/package.json @@ -0,0 +1,28 @@ +{ + "name": "js-small-apps", + "description": "A collection of small apps and exercises implemented with Javascript.", + "version": "1.0.0", + "license": "MIT", + "author": { + "name": "Armand Philippot", + "url": "https://www.armandphilippot.com" + }, + "homepage": "https://github.com/ArmandPhilippot/js-small-apps#readme", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:ArmandPhilippot/js-small-apps.git" + }, + "bugs": { + "url": "https://github.com/ArmandPhilippot/js-small-apps/issues" + }, + "private": true, + "scripts": { + "release": "standard-version -s" + }, + "devDependencies": { + "@commitlint/cli": "^16.2.1", + "@commitlint/config-conventional": "^16.2.1", + "husky": "^7.0.2", + "standard-version": "^9.3.1" + } +} diff --git a/public/projects/js-small-apps/rock-paper-scissors/README.md b/public/projects/js-small-apps/rock-paper-scissors/README.md new file mode 100644 index 0000000..834c716 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/README.md @@ -0,0 +1,11 @@ +# Rock Paper Scissors + +A Javascript implementation of the game. + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#rps-game + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/rock-paper-scissors/app.js b/public/projects/js-small-apps/rock-paper-scissors/app.js new file mode 100644 index 0000000..581b442 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/app.js @@ -0,0 +1,115 @@ +import RPSGame from "./lib/class-rps-game.js"; + +function createPreview() { + const preview = document.querySelector(".preview__content"); + const player1Wrapper = document.createElement("div"); + const player2Wrapper = document.createElement("div"); + const vsWrapper = document.createElement("div"); + player1Wrapper.id = "player1Wrapper"; + player2Wrapper.id = "player2Wrapper"; + vsWrapper.textContent = "vs"; + vsWrapper.style.fontWeight = 600; + preview.append(player1Wrapper, vsWrapper, player2Wrapper); +} + +function getUserPreviewId(id) { + let userId; + if (id === "player1-name" || id === "player1-ia") { + userId = "player1Wrapper"; + } else if (id === "player2-name" || id === "player2-ia") { + userId = "player2Wrapper"; + } + return userId; +} + +function fillPreview(target) { + const userId = getUserPreviewId(target.id); + if (userId) { + const dest = document.getElementById(userId); + dest.textContent = target.value; + } +} + +function toggleIABadge(target) { + const userId = getUserPreviewId(target.id); + if (userId) { + const dest = document.getElementById(userId); + target.checked + ? dest.classList.add("ia-badge") + : dest.classList.remove("ia-badge"); + } +} + +function startGame(player1, player2, maxRound) { + const register = document.querySelector(".register"); + const game = document.querySelector(".game"); + const buttons = { + rock: document.querySelector(".btn--rock"), + paper: document.querySelector(".btn--paper"), + scissors: document.querySelector(".btn--scissors"), + newGame: document.querySelector(".btn--new-game"), + }; + const p1Scoring = { + name: document.getElementById("player1username"), + value: document.getElementById("player1score"), + }; + const p2Scoring = { + name: document.getElementById("player2username"), + value: document.getElementById("player2score"), + }; + const messages = document.querySelector(".message-board"); + const players = []; + players.push(player1); + players.push(player2); + + const app = new RPSGame(players, buttons, p1Scoring, p2Scoring, messages); + app.init(); + app.maxRound = maxRound && maxRound !== "0" ? maxRound : ""; + register.style.display = "none"; + game.style.display = "flex"; +} + +function listen() { + const inputP1Name = document.getElementById("player1-name"); + const inputP2Name = document.getElementById("player2-name"); + const checkboxP1IA = document.getElementById("player1-ia"); + const checkboxP2IA = document.getElementById("player2-ia"); + const inputMaxRound = document.getElementById("round-number"); + const registerBtn = document.querySelector(".form__submit"); + + if (inputP1Name.value) fillPreview(inputP1Name); + if (inputP2Name.value) fillPreview(inputP2Name); + if (checkboxP1IA.checked) toggleIABadge(checkboxP1IA); + if (checkboxP2IA.checked) toggleIABadge(checkboxP2IA); + + inputP1Name.addEventListener("keyup", (e) => fillPreview(e.target)); + inputP2Name.addEventListener("keyup", (e) => fillPreview(e.target)); + checkboxP1IA.addEventListener("change", (event) => { + toggleIABadge(event.target); + if (checkboxP2IA.checked && event.target.checked) { + checkboxP2IA.checked = false; + toggleIABadge(checkboxP2IA); + } + }); + checkboxP2IA.addEventListener("change", (event) => { + toggleIABadge(event.target); + if (checkboxP1IA.checked && event.target.checked) { + checkboxP1IA.checked = false; + toggleIABadge(checkboxP1IA); + } + }); + registerBtn.addEventListener("click", (event) => { + event.preventDefault(); + const player1 = { username: inputP1Name.value, ia: checkboxP1IA.checked }; + const player2 = { username: inputP2Name.value, ia: checkboxP2IA.checked }; + const maxRound = inputMaxRound.value; + startGame(player1, player2, maxRound); + }); +} + +function init() { + createPreview(); + listen(); +} + +init(); diff --git a/public/projects/js-small-apps/rock-paper-scissors/index.html b/public/projects/js-small-apps/rock-paper-scissors/index.html new file mode 100644 index 0000000..19c9d7a --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/index.html @@ -0,0 +1,142 @@ +<!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>Rock Paper Scissors - Game</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="branding"> + <h1 class="branding__title">Rock Paper Scissors</h1> + </header> + <main class="main"> + <div class="register"> + <form action="#" class="form form--register"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Player 1</legend> + <div class="form__item"> + <label class="form__label" for="player1-name">Name:</label> + <input + type="text" + id="player1-name" + name="player1-name" + placeholder="Pseudo" + class="form__input" + /> + </div> + <div class="form__item"> + <input + type="checkbox" + name="player1-ia" + id="player1-ia" + class="form__checkbox" + /> + <label class="form__label form__label--checkbox" for="player1-ia" + >IA?</label + > + </div> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Player 2</legend> + <div class="form__item"> + <label class="form__label" for="player2-name">Name:</label> + <input + type="text" + id="player2-name" + name="player2-name" + placeholder="Pseudo" + class="form__input" + /> + </div> + <div class="form__item"> + <input + type="checkbox" + name="player2-ia" + id="player2-ia" + class="form__checkbox" + /> + <label class="form__label form__label--checkbox" for="player2-ia" + >IA?</label + > + </div> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Settings</legend> + <div class="form__item"> + <label class="form__label" for="round-number" + >Number of rounds:</label + > + <input + type="number" + name="round-number" + id="round-number" + class="form__input" + /> + <p class="form__hint"> + Note: If empty or 0, your party will never end! + </p> + </div> + </fieldset> + <button type="submit" class="form__submit btn btn--register"> + Play! + </button> + </form> + <div class="register__preview"> + <div class="preview"> + <div class="preview__content"></div> + </div> + </div> + </div> + <div class="game"> + <div class="scoring-board"> + <div class="scoring-board__item"> + <div id="player1username" class="scoring-board__username"> + Player1 + </div> + <div id="player1score" class="scoring-board__score">0</div> + </div> + <div class="scoring-board__item"> + <div id="player2username" class="scoring-board__username"> + Player2 + </div> + <div id="player2score" class="scoring-board__score">0</div> + </div> + </div> + <div class="message-board">Ready? Let's play!</div> + <div class="actions"> + <button type="button" class="actions__body btn btn--action btn--rock"> + <span class="actions__txt screen-reader-txt">Rock</span> + <span class="actions__icon">✊</span> + </button> + <button + type="button" + class="actions__body btn btn--action btn--paper" + > + <span class="actions__txt screen-reader-txt">Paper</span> + <span class="actions__icon">✋</span> + </button> + <button + type="button" + class="actions__body btn btn--action btn--scissors" + > + <span class="actions__txt screen-reader-txt">Scissors</span> + <span class="actions__icon">✌️</span> + </button> + </div> + <div class="settings"> + <button type="reset" class="settings__body btn btn--new-game"> + New Game? + </button> + </div> + </div> + </main> + <footer class="footer"> + <p class="footer__copyright"> + Rock Paper Scissors. MIT 2021. Armand Philippot. + </p> + </footer> + <script type="module" src="./app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js new file mode 100644 index 0000000..92898a1 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js @@ -0,0 +1,296 @@ +import Player from "./class-player.js"; + +/** + * Game class. + */ +class Game { + #name = "My Game"; + #language = "en-US"; + #playerId = 0; + #players = []; + #roundWinners = []; + #roundLosers = []; + #gameWinners = []; + #gameLosers = []; + #state = "paused"; + #turn; + #currentTurn = 1; + #maxTurn = 0; + #currentRound = 1; + #maxRound = 0; + + /** + * Initialize a new Game instance. + * @param {String} name - The game name. + * @param {Object[]} players - The players. + * @param {String} players[].username - The player username. + * @param {Boolean} players[].ia - True to set the player as an IA. + */ + constructor(name, players) { + this.#name = name; + players.forEach((player) => { + this.#players.push( + new Player(++this.#playerId, player.username, player.ia) + ); + }); + } + + set name(string) { + this.#name = string; + } + + get name() { + return this.#name; + } + + set language(languageCode) { + this.#language = languageCode; + } + + get language() { + return this.#language; + } + + set players(array) { + array.forEach((player) => + this.#players.push( + new Player(++this.#playerId, player.username, player.ia) + ) + ); + } + + get players() { + return this.#players; + } + + set roundWinners(array) { + if (array.length > 0) { + array.forEach((player) => this.#roundWinners.push(player)); + } else { + this.#roundWinners = []; + } + } + + get roundWinners() { + return this.#roundWinners; + } + + set roundLosers(array) { + if (array.length > 0) { + array.forEach((player) => this.#roundLosers.push(player)); + } else { + this.#roundLosers = []; + } + } + + get roundLosers() { + return this.#roundLosers; + } + + set gameWinners(array) { + if (array.length > 0) { + array.forEach((player) => this.#gameWinners.push(player)); + } else { + this.#gameWinners = []; + } + } + + get gameWinners() { + return this.#gameWinners; + } + + set gameLosers(array) { + if (array.length > 0) { + array.forEach((player) => this.#gameLosers.push(player)); + } else { + this.#gameLosers = []; + } + } + + get gameLosers() { + return this.#gameLosers; + } + + set state(string) { + this.#state = string; + } + + get state() { + return this.#state; + } + + set turn(generator) { + this.#turn = generator; + } + + get turn() { + return this.#turn; + } + + set currentTurn(int) { + this.#currentTurn = int; + } + + get currentTurn() { + return this.#currentTurn; + } + + set maxTurn(int = this.getPlayersNumber()) { + this.#maxTurn = int; + } + + get maxTurn() { + return this.#maxTurn; + } + + set currentRound(int) { + this.#currentRound = int; + } + + get currentRound() { + return this.#currentRound; + } + + set maxRound(int) { + this.#maxRound = int; + } + + get maxRound() { + return this.#maxRound; + } + + newPlayer(username) { + this.players.push(new Player(++this.#playerId, username)); + } + + getPlayersNumber() { + return this.players.length; + } + + getPlayer(number) { + return this.players[number - 1]; + } + + getCurrentPlayer() { + return this.getPlayer(this.currentTurn); + } + + getNextPlayer() { + if (this.currentTurn < this.maxTurn) { + return this.getPlayer(this.currentTurn + 1); + } else { + return this.getPlayer(1); + } + } + + isFirstTurn() { + return this.currentTurn === 1; + } + + isNewRound() { + return this.currentRound > 1 && this.isFirstTurn(); + } + + isGameOver() { + return this.state === "ended"; + } + + resetScore() { + this.players.forEach((player) => { + player.score = 0; + }); + } + + newGame() { + this.roundWinners = []; + this.roundLosers = []; + this.gameWinners = []; + this.gameLosers = []; + this.state = "paused"; + this.turn = this.#generateTurns(); + this.currentTurn = 1; + this.currentRound = 1; + this.maxTurn = this.getPlayersNumber(); + this.resetScore(); + } + + setPlayerChoice(choice) { + if (this.state === "running") { + this.getCurrentPlayer().choice = choice; + } + } + + *#generateTurns() { + this.currentRound = 1; + while (this.maxRound ? this.currentRound <= this.maxRound : true) { + this.currentTurn = 1; + while (this.currentTurn <= this.maxTurn) { + yield this.currentTurn; + this.currentTurn++; + } + this.currentRound++; + } + this.stop(); + this.setGameWinners(); + this.setGameLosers(); + return; + } + + /** + * Get a random choice from an array of choices. + * @param {Array} array - The choices. + * @returns {*} A random choice. + */ + getRandomChoice(array) { + const randomIndex = Math.floor(Math.random() * array.length); + return array[randomIndex]; + } + + getOrderedScores() { + let scores = []; + this.players.forEach((player) => { + scores.push(player.score); + }); + scores.sort((a, b) => a - b); + + return scores; + } + + setGameWinners() { + const scores = this.getOrderedScores(); + const highestScore = scores.pop(); + const winners = this.players.filter( + (player) => player.score === highestScore + ); + this.gameWinners = winners; + } + + setGameLosers() { + const scores = this.getOrderedScores(); + const lowestScore = scores.shift(); + const losers = this.players.filter( + (player) => player.score === lowestScore + ); + this.gameLosers = losers; + } + + resume() { + this.state = "running"; + } + + pause() { + this.state = "paused"; + } + + stop() { + this.state = "ended"; + } + + launch() { + this.newGame(); + this.resume(); + this.turn.next(); + } +} + +export default Game; diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js new file mode 100644 index 0000000..8935581 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js @@ -0,0 +1,60 @@ +/** + * Player class. + */ +class Player { + #id; + #username = "Anonymous"; + #choice = ""; + #score = 0; + #ia = false; + + /** + * Initialize a new Player instance. + * @param {Integer} id - The player id. + * @param {String} username - The player username. + * @param {Boolean} ia - True to set player as an IA. + */ + constructor(id, username, ia) { + this.#id = id; + this.#username = username; + this.#ia = ia; + } + + get id() { + return this.#id; + } + + set username(name) { + this.#username = name; + } + + get username() { + return this.#username; + } + + set choice(choice) { + this.#choice = choice; + } + + set score(score) { + this.#score = score; + } + + get choice() { + return this.#choice; + } + + get score() { + return this.#score; + } + + set ia(boolean) { + this.#ia = boolean; + } + + get ia() { + return this.#ia; + } +} + +export default Player; diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js new file mode 100644 index 0000000..fe517db --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js @@ -0,0 +1,230 @@ +import Game from "./class-game.js"; + +/** + * RPSGame class. + */ +class RPSGame extends Game { + #choices = ["rock", "paper", "scissors"]; + #buttons = { rock: "", paper: "", scissors: "", newGame: "" }; + #p1Scoring = { name: "", value: "" }; + #p2Scoring = { name: "", value: "" }; + #messages = ""; + #messageIterator; + #timeoutId; + + /** + * Initialize a new RPSGame instance. + * @param {Object[]} players - An array of player object. + * @param {String} players[].username - The player username. + * @param {Boolean} players[].ia - True to set the player as an IA. + * @param {Object} buttons - The buttons HTMLElement. + * @param {HTMLElement} buttons.rock - Button Element for rock choice. + * @param {HTMLElement} buttons.paper - Button Element for paper choice. + * @param {HTMLElement} buttons.scissors - Button Element for scissors choice. + * @param {HTMLElement} buttons.newGame - Button Element to start new game. + * @param {Object} p1Scoring - The player 1 scoring display. + * @param {HTMLElement} p1Scoring.name - Element to display player 1 name. + * @param {HTMLElement} p1Scoring.value - Element to display player 1 score. + * @param {Object} p2Scoring - The player 2 scoring display. + * @param {HTMLElement} p2Scoring.name - Element to display player 2 name. + * @param {HTMLElement} p2Scoring.value - Element to display player 2 score. + * @param {HTMLElement} messages - Element to display turn/game results. + */ + constructor( + players, + buttons = { rock: "", paper: "", scissors: "", newGame: "" }, + p1Scoring = { name: "", value: "" }, + p2Scoring = { name: "", value: "" }, + messages + ) { + super("Rock Paper Scissors", players); + this.#buttons = buttons; + this.#p1Scoring = p1Scoring; + this.#p2Scoring = p2Scoring; + this.#messages = messages; + } + + get messages() { + return this.#messages; + } + + set messageIterator(generator) { + this.#messageIterator = generator; + } + + get messageIterator() { + return this.#messageIterator; + } + + #updatePlayers() { + this.#p1Scoring.name.textContent = this.getPlayer(1).username; + this.#p2Scoring.name.textContent = this.getPlayer(2).username; + } + + #updateScore() { + this.#p1Scoring.value.textContent = this.getPlayer(1).score; + this.#p2Scoring.value.textContent = this.getPlayer(2).score; + } + + async #createMessage(msg, delay = 0) { + return new Promise( + (resolve) => + (this.#timeoutId = setTimeout(() => { + resolve(msg); + }, delay)) + ); + } + + async *#generateMessages() { + let msg; + msg = yield await this.#createMessage("New game, let's play!"); + + while (this.state === "running") { + for (let index = 0; index < this.getPlayersNumber(); index++) { + if (this.getCurrentPlayer().ia) { + msg = yield this.#createMessage( + `${this.getCurrentPlayer().username} is playing...`, + this.isFirstTurn() ? 1200 : 200 + ); + } else { + msg = yield this.#createMessage( + `${this.getCurrentPlayer().username}'s turn...`, + this.isFirstTurn() ? 1200 : 900 + ); + } + } + msg = yield this.#createMessage( + msg, + this.getCurrentPlayer().ia ? 1000 : 500 + ); + if (!this.isGameOver()) { + msg = yield this.#createMessage("New round...", 1500); + } + } + const winnersList = this.gameWinners.map((winner) => winner.username); + const losersList = this.gameLosers.map((loser) => loser.username); + msg = yield this.#createMessage( + `Winner: ${winnersList.join(", ")} / Loser: ${losersList.join(", ")}` + ); + } + + async printNextMessage(msg = null) { + if (!this.messageIterator) { + this.messageIterator = this.#generateMessages(); + } + this.messages.textContent = await this.messageIterator + .next(msg) + .then((object) => object.value); + } + + async #setTurnIssue() { + const choices = `${this.getPlayer(1).choice}-${this.getPlayer(2).choice}`; + let turnWinner; + let turnLoser; + let even = false; + let msg; + + switch (choices) { + case "rock-paper": + case "paper-scissors": + case "scissors-rock": + turnWinner = this.getPlayer(2); + turnLoser = this.getPlayer(1); + break; + case "paper-rock": + case "rock-scissors": + case "scissors-paper": + turnWinner = this.getPlayer(1); + turnLoser = this.getPlayer(2); + break; + default: + even = true; + break; + } + + if (!even) { + this.turnWinners = [turnWinner]; + this.turnLosers = [turnLoser]; + turnWinner.score++; + msg = `${turnWinner.username} wins! ${turnWinner.choice} beats ${turnLoser.choice}.`; + } else { + msg = `No winner. ${this.getPlayer(1).choice} equals to ${ + this.getPlayer(2).choice + }.`; + } + await this.printNextMessage(msg); + this.#updateScore(); + await this.printNextMessage(); + } + + async #getIAAction() { + if (this.currentTurn % 2 === 0) { + await this.#setTurnIssue(); + this.turn.next(); + await this.printNextMessage(); + if (!this.isGameOver()) { + this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices); + } + } else { + this.turn.next(); + await this.printNextMessage(); + this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices); + await this.#setTurnIssue(); + } + this.turn.next(); + await this.printNextMessage(); + } + + async listen() { + for (const [name, element] of Object.entries(this.#buttons)) { + element.addEventListener("click", async (event) => { + event.preventDefault(); + switch (name) { + case "rock": + case "paper": + case "scissors": + if (this.state === "running") { + this.setPlayerChoice(name); + if (this.currentTurn % 2 === 0) { + await this.#setTurnIssue(); + } + if (this.getNextPlayer().ia) { + await this.#getIAAction(); + } else { + this.turn.next(); + await this.printNextMessage(); + } + } + break; + case "newGame": + this.messageIterator = null; + clearTimeout(this.#timeoutId); + await this.launch(); + default: + break; + } + }); + } + } + + async launch() { + super.launch(); + this.#updatePlayers(); + this.#updateScore(); + await this.printNextMessage(); + await this.printNextMessage(); + + if (this.getCurrentPlayer().ia) { + this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices); + this.turn.next(); + await this.printNextMessage(); + } + } + + async init() { + await this.launch(); + this.listen(); + } +} + +export default RPSGame; diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js b/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js new file mode 100644 index 0000000..73c5888 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js @@ -0,0 +1,70 @@ +import RPS_Game from "./rps-game.js"; + +export default class RPS_Instance extends RPS_Game { + #buttons = {}; + #scoring = { player1: '', player2: '' }; + #resultBox = ''; + #resultMsg = ''; + + constructor(player1, player2, buttons, scoring, result) { + super(player1, player2); + this.#buttons = buttons; + this.#scoring = scoring; + this.#resultBox = result; + this.#resultMsg = result.innerHTML; + } + + updateScoring() { + for (const [name, element] of Object.entries(this.#scoring)) { + if ( 'player1' === name ) { + element.innerHTML = this.player1Score; + } + + if ( 'player2' === name ) { + element.innerHTML = this.player2Score; + } + } + } + + updateResult(result = '') { + let txt; + if ( result === 'player1' ) { + txt = this.player1Choice + ' beats ' + this.player2Choice + ".<br>You win!" + this.#resultBox.style.color = 'green'; + } else if ( result === 'player2' ) { + txt = this.player1Choice + ' loses to ' + this.player2Choice + ".<br>You lose..." + this.#resultBox.style.color = 'red'; + } else if ( result === 'even' ) { + txt = this.player1Choice + ' equals to ' + this.player2Choice + ".<br>No winner."; + this.#resultBox.style.color = 'black'; + } else { + txt = this.#resultMsg; + this.#resultBox.style.color = ''; + } + + this.#resultBox.innerHTML = txt; + } + + listen() { + for (const [name, element] of Object.entries(this.#buttons)) { + element.addEventListener('click', () => { + if ( 'reset' === name ) { + this.reset(); + this.updateResult(); + this.updateScoring(); + } else { + this.player1Choice = name; + const result = this.calculateScore(); + this.setScore(result); + this.updateResult(result); + this.updateScoring(); + } + }) + } + } + + init() { + this.reset(); + this.listen(); + } +} diff --git a/public/projects/js-small-apps/rock-paper-scissors/style.css b/public/projects/js-small-apps/rock-paper-scissors/style.css new file mode 100644 index 0000000..28d2f03 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/style.css @@ -0,0 +1,433 @@ +/* + * Base + */ + +*, +*::after, +*::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + display: flex; + flex-flow: column nowrap; + background: hsl(180, 29%, 95%); + font-family: "Courier New", Courier, monospace; + font-size: 1rem; + line-height: 1.618; + min-height: 100vh; + max-width: 100vw; +} + +button { + display: block; + cursor: pointer; + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; +} + +/* + * Helpers + */ + +.screen-reader-txt { + border: 0; + clip: rect(1px, 1px, 1px, 1px); + width: 1px; + height: 1px; + overflow: hidden; + padding: 0; + position: absolute !important; + word-break: normal; + word-wrap: normal !important; +} + +/* + * Layout + */ + +.branding { + margin: 2rem auto clamp(2rem, 5vh, 5rem); +} + +.branding__title { + color: hsl(207, 85%, 47%); + font-size: clamp(2.5rem, 3vw, 3rem); + text-align: center; + text-shadow: 1px 1px 1px hsl(207, 85%, 27%); +} + +.main { + display: flex; + flex-flow: column nowrap; + flex: 1; + margin: 3rem 0; +} + +.register { + flex: 1; + display: grid; + grid-template-columns: repeat( + auto-fill, + min(calc(100vw - 2rem), calc(1200px / 2 - 4rem)) + ); + gap: 2rem; + align-items: center; + justify-content: center; + justify-items: center; + margin: auto; + width: min(calc(100vw - 2rem), calc(1200px - 2rem)); +} + +.form--register { + justify-self: stretch; +} + +.form { + display: flex; + flex-flow: column wrap; + gap: clamp(1rem, 3vh, 2rem); +} + +.form__fieldset { + display: flex; + flex-flow: row wrap; + align-items: flex-end; + gap: 2rem; + border: 4px solid hsl(207, 85%, 45%); + padding: 1.7rem clamp(0.5rem, 3vw, 1.5rem); + position: relative; + margin-top: 2.5rem; +} + +.form__legend { + background: hsl(180, 29%, 95%); + border: 4px solid hsl(207, 85%, 45%); + border-bottom-width: 0; + color: hsl(207, 85%, 28%); + font-size: 1.4rem; + font-weight: 600; + letter-spacing: 2px; + text-transform: uppercase; + padding: 0.5rem 1.3rem; + position: absolute; + top: -40px; + left: -4px; + right: -4px; +} + +.form__item { + flex: 1 1 max-content; +} + +.form__item:last-child:not(:only-of-type) { + margin-left: auto; + margin-bottom: 0.8rem; +} + +.form__label { + display: block; + cursor: pointer; + position: relative; + letter-spacing: 1px; +} + +.form__label--checkbox { + display: initial; +} + +.form__label--checkbox::before { + position: absolute; + top: -1px; + left: -28px; + display: block; + content: ""; + width: 1.2rem; + height: 1.2rem; + background: hsl(180, 29%, 95%); + border: 3px solid hsl(207, 85%, 45%); + color: hsl(207, 85%, 40%); + font-size: 1.1rem; + font-weight: 600; + line-height: 0.85; + text-align: center; +} + +.form__input { + background: hsl(180, 29%, 95%); + border: 3px solid hsl(207, 85%, 45%); + font-size: inherit; + padding: 0.7rem 0.8rem; +} + +.form__checkbox { + order: 2; + width: max-content; +} + +.form__checkbox:checked ~ .form__label--checkbox::before { + content: "x"; +} + +.form__hint { + font-size: 0.85rem; + margin: 1rem 0 0; +} + +.form__submit { + margin: 1rem 0 0 auto; +} + +.preview { + background: hsl(207, 85%, 45%); + clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); + width: clamp(18rem, 40vw, 30rem); + height: clamp(18rem, 40vw, 30rem); + position: relative; +} + +.preview__content { + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + gap: 1.5rem; + background: hsl(180, 29%, 95%); + border-radius: 50%; + font-size: clamp(1.2rem, 3vw, 2rem); + width: 80%; + height: 80%; + padding: 1rem; + position: relative; + top: 10%; + left: 10%; +} + +.ia-badge { + position: relative; +} + +.ia-badge::after { + display: block; + content: "IA"; + position: absolute; + top: 0; + right: -2.5rem; + background: rgb(253, 206, 119); + border-radius: 50%; + font-size: 0.9rem; + padding: 4px 8px; + width: max-content; +} + +.game { + flex: 1; + display: none; + flex-flow: column nowrap; + justify-content: space-between; + align-items: center; + gap: 2rem; + margin: auto; + padding: clamp(1rem, 3vh, 2rem) 0; + width: min(calc(100vw - 2rem), 600px); +} + +.scoring-board { + font-family: "Courier New", Courier, monospace; + font-size: 1.5rem; + text-align: center; + width: 100%; +} + +.scoring-board__item { + background: hsl(0, 0%, 100%); + border: 5px solid hsl(207, 85%, 47%); + border-radius: 3px; + display: inline-block; + margin-bottom: clamp(1rem, 3vh, 2rem); + padding: 1rem 0; + width: calc(50% - 1rem); + position: relative; +} + +.scoring-board__item:first-child { + box-shadow: 1px 1px 0 2px hsl(207, 85%, 27%); +} + +.scoring-board__item:not(:first-child) { + box-shadow: -1px 1px 0 2px hsl(207, 85%, 27%); + margin-left: 1rem; +} + +.scoring-board__item:first-child::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + right: -50%; + background: hsl(207, 85%, 47%); + box-shadow: 0 3px 0 0 hsl(207, 85%, 27%); + height: 5px; + z-index: -1; +} + +.scoring-board__username { + font-variant: small-caps; + letter-spacing: 1px; +} + +.scoring-board__score { + font-size: 2rem; + font-weight: 600; + margin-top: 1rem; +} + +.message-board { + font-size: 1.4rem; + text-align: center; + margin: 1rem auto; +} + +.actions { + display: grid; + grid-template-columns: repeat(3, 33%); + justify-content: center; + gap: clamp(1rem, 3vw, 2rem); + font-size: 1.2rem; + margin-bottom: clamp(1rem, 3vh, 2rem); +} + +.actions__body { + width: clamp(5rem, 15vw, 8rem); + height: clamp(4.5rem, 12vw, 7rem); + position: relative; +} + +.actions__icon { + font-size: clamp(2.2rem, 7vw, 3.2rem); + filter: drop-shadow(1px 1px 1px hsl(207, 85%, 27%)); + position: relative; + top: 4px; +} + +.settings { + display: flex; + flex-flow: row wrap; + gap: 2rem; + justify-content: center; +} + +.footer { + font-size: 0.9rem; + text-align: center; + margin: clamp(2rem, 5vh, 4rem) auto 2rem; +} + +/* + * Components + */ + +.btn { + font-weight: 600; + transition: all 0.2s ease-in-out 0s; +} + +.btn:focus { + outline: none; +} + +.btn--action { + background: hsl(207, 85%, 45%); + border: 2px solid hsl(207, 85%, 28%); + border-radius: 100%; + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 5px 0 0 hsl(207, 85%, 28%), + 0 5px 0 2px hsl(207, 85%, 20%); +} + +.btn--action:hover, +.btn--action:focus { + background: hsl(207, 85%, 52%); + transform: translateY(2px); +} + +.btn--action:hover { + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 3px 0 0 hsl(207, 85%, 32%), + 0 3px 0 2px hsl(207, 85%, 22%); +} + +.btn--action:focus { + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 3px 0 0 hsl(207, 85%, 32%), + 0 3px 0 2px hsl(207, 85%, 22%), 0 3px 0 8px hsl(204, 42%, 68%); +} + +.btn--action:active { + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 0 0 0 hsl(207, 85%, 28%), + 0 0 0 1px hsl(207, 85%, 20%); + transform: translateY(8px); +} + +.btn--new-game { + border: 2px solid hsl(0, 50%, 45%); + border-radius: 0.5rem; + color: hsl(0, 50%, 45%); + box-shadow: 1px 1px 0 1px hsl(0, 50%, 25%); + padding: 0.75rem 1rem; +} + +.btn--new-game:hover, +.btn--new-game:focus { + background: hsl(0, 35%, 90%); + color: hsl(0, 50%, 25%); + transform: translateX(-3px) translateY(-3px); +} + +.btn--new-game:hover { + box-shadow: 3px 3px 0 1px hsl(0, 50%, 25%); +} + +.btn--new-game:focus { + box-shadow: 3px 3px 0 1px hsl(0, 50%, 25%), 3px 3px 0 4px hsl(0, 11%, 75%); +} + +.btn--new-game:active { + background: hsl(204, 14%, 93%); + color: hsl(0, 50%, 25%); + box-shadow: none; + transform: translateX(3px) translateY(3px); +} + +.btn--register { + border: 3px solid hsl(207, 85%, 50%); + border-radius: 8px; + background-color: hsl(207, 85%, 40%); + box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%), + 3px 3px 0 2px hsl(207, 85%, 30%); + color: #fff; + font-size: 1.2rem; + font-variant: small-caps; + font-weight: 600; + text-shadow: 2px 2px 0 #000; + width: max-content; + padding: 1rem 2rem; +} + +.btn--register:hover, +.btn--register:focus { + background-color: hsl(207, 85%, 47%); + border-color: hsl(207, 85%, 57%); + box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%), + 5px 5px 0 2px hsl(207, 85%, 30%); + transform: translateY(-3px) translateX(-3px); +} + +.btn--register:active { + background-color: hsl(207, 85%, 35%); + border-color: hsl(207, 85%, 45%); + box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%), + 0 0 0 1px hsl(207, 85%, 30%); + transform: translateY(3px) translateX(3px); +} diff --git a/public/projects/js-small-apps/users-list/README.md b/public/projects/js-small-apps/users-list/README.md new file mode 100644 index 0000000..4c10232 --- /dev/null +++ b/public/projects/js-small-apps/users-list/README.md @@ -0,0 +1,18 @@ +# Users list + +An users list implementation using Javascript. + +The implementation is inspired by the [CauseEffect app from @florinpop17 repo](https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Cause-Effect-App.md). + +However, I made small changes: + +- Instead of hard coding the users objects, this implementation uses `fetch()` to retrieve data from an API. +- I implemented "routing": user info can be displayed if URL contains the user parameter. + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#users-list + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/users-list/app.js b/public/projects/js-small-apps/users-list/app.js new file mode 100644 index 0000000..c9f998d --- /dev/null +++ b/public/projects/js-small-apps/users-list/app.js @@ -0,0 +1,187 @@ +/** + * Retrieve data from an API. + * @param {String} api - The API url. + * @returns {Promise} The result from api fetching. + */ +async function fetchData(api) { + const headers = new Headers(); + const init = { + method: "GET", + headers, + mode: "cors", + cache: "default", + }; + const response = await fetch(api, init); + const result = await response.json(); + return result; +} + +/** + * Create a list item with a link. + * @param {Object} data - An object containing an id and an username. + * @returns {HTMLElement} A list item containing a link. + */ +function getListItem(data) { + const item = document.createElement("li"); + const link = document.createElement("a"); + link.textContent = data.username; + link.href = `?user=${data.id}`; + link.id = `user-${data.id}`; + link.classList.add("users-list__link"); + item.appendChild(link); + item.classList.add("users-list__item"); + return item; +} + +/** + * Print the users list. + * @param {Object[]} users - An array of user object. + */ +function printUsersList(users) { + const list = document.querySelector(".users-list"); + users.map((user) => list.appendChild(getListItem(user))); +} + +/** + * Create a description term element. + * @param {String} body - The description term body. + * @returns {HTMLElement} The description term element. + */ +function createUserInfoDt(body) { + const dt = document.createElement("dt"); + dt.classList.add("user-info__label"); + dt.textContent = body; + return dt; +} + +/** + * Create a description details element. + * @param {String} body - The description details body. + * @param {Boolean} [hasHTML] - True if body contains HTML; false otherwise. + * @returns {HTMLElement} The description details element. + */ +function createUserInfoDd(body, hasHTML = false) { + const dd = document.createElement("dd"); + dd.classList.add("user-info__content"); + hasHTML ? (dd.innerHTML = body) : (dd.textContent = body); + return dd; +} + +/** + * Get the markup to display info for a given user. + * @param {Object} user - An user with fullname, email address, phone & website. + * @returns {Object} Two HTMLElement: title and body. + */ +function getUserInfoTemplate(user) { + const userAddress = `${user.address.suite}<br />${user.address.street}<br />${user.address.zipcode} ${user.address.city}`; + + const fullName = document.createElement("h2"); + const userDetails = document.createElement("dl"); + const emailLabel = createUserInfoDt("Email"); + const email = createUserInfoDd(user.email); + const addressLabel = createUserInfoDt("Address"); + const address = createUserInfoDd(userAddress, true); + const phoneLabel = createUserInfoDt("Phone"); + const phone = createUserInfoDd(user.phone); + const websiteLabel = createUserInfoDt("Website"); + const website = createUserInfoDd(user.website); + + fullName.textContent = user.name; + fullName.classList.add("user-info__title"); + userDetails.classList.add("user-info__body"); + userDetails.append( + emailLabel, + email, + addressLabel, + address, + phoneLabel, + phone, + websiteLabel, + website + ); + + return { + title: fullName, + body: userDetails, + }; +} + +/** + * Print the info for a given user. + * @param {Integer} userId - The user id. + */ +async function printUserInfo(userId) { + const api = `https://jsonplaceholder.typicode.com/users/${userId}`; + const user = await fetchData(api).then((data) => data); + const userInfo = document.querySelector(".user-info"); + const userInfoTemplate = getUserInfoTemplate(user); + userInfo.hasChildNodes() + ? userInfo.replaceChildren(userInfoTemplate.title, userInfoTemplate.body) + : userInfo.append(userInfoTemplate.title, userInfoTemplate.body); +} + +/** + * Add a class "active" to a link. + * @param {Integer} linkId - The link id. + */ +function addActiveClassTo(linkId) { + const link = document.getElementById(linkId); + link.classList.add("active"); +} + +/** + * Print the user info on click. + * @param {MouseEvent} e - The click event. + * @param {NodeList} links - The users links. + */ +function handleClick(e, links) { + e.preventDefault(); + const userId = e.target.id.split("user-")[1]; + const linkId = `user-${userId}`; + history.pushState({ userId }, e.target.textContent, e.target.href); + printUserInfo(userId); + + for (let i = 0; i < links.length; i++) { + links[i].classList.remove("active"); + } + + addActiveClassTo(linkId); +} + +/** + * Listen all users links. + */ +function listenUsersLinks() { + const usersLinks = document.querySelectorAll(".users-list__link"); + for (let i = 0; i < usersLinks.length; i++) { + const link = usersLinks[i]; + link.addEventListener("click", (e) => handleClick(e, usersLinks)); + } +} + +/** + * Load user info when URL refers to an user page. + */ +function listenURL() { + const currentURL = window.location.href; + const isUserPage = currentURL.includes("user="); + if (isUserPage) { + const userId = currentURL.split("?user=")[1]; + const linkId = `user-${userId}`; + printUserInfo(userId); + addActiveClassTo(linkId); + } +} + +/** + * Init the app. + */ +async function init() { + const api = "https://jsonplaceholder.typicode.com/users"; + const users = await fetchData(api).then((data) => data); + printUsersList(users); + listenUsersLinks(); + listenURL(); +} + +init(); diff --git a/public/projects/js-small-apps/users-list/index.html b/public/projects/js-small-apps/users-list/index.html new file mode 100644 index 0000000..6472ffe --- /dev/null +++ b/public/projects/js-small-apps/users-list/index.html @@ -0,0 +1,29 @@ +<!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>User list</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="header"> + <h1 class="branding">Users list</h1> + </header> + <main class="main"> + <div class="users"> + <h2 class="users__title">Users</h2> + <p class="users__instructions"> + Select an user to see its personal details. + </p> + <ul class="users-list"></ul> + </div> + <div class="user-info"></div> + </main> + <footer class="footer"> + <p class="copyright">Users list. MIT 2021. Armand Philippot.</p> + </footer> + <script src="app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/users-list/style.css b/public/projects/js-small-apps/users-list/style.css new file mode 100644 index 0000000..4923c6f --- /dev/null +++ b/public/projects/js-small-apps/users-list/style.css @@ -0,0 +1,104 @@ +*, +*::after, +*::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #fff; + color: #000; + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 1.618; + display: flex; + flex-flow: column; + min-height: 100vh; +} + +.header, +.main, +.footer { + width: min(calc(100vw - 2rem), 100ch); + margin-left: auto; + margin-right: auto; +} + +.header, +.footer { + padding: 1rem 0; + text-align: center; +} + +.branding { + color: hsl(219, 64%, 35%); +} + +.main { + flex: 1; + display: flex; + flex-flow: row wrap; + gap: 1rem; + margin: 2rem auto; +} + +.users, +.user-info { + border: 2px solid hsl(219, 64%, 35%); +} + +.users { + flex: 1 1 35ch; + padding: 1rem 0; +} + +.users__title, +.users__instructions { + padding: 0 1rem; +} + +.users-list { + list-style-type: none; + margin-top: 1rem; +} + +.users-list__link { + display: block; + padding: 0.2rem 1rem; + color: hsl(219, 64%, 35%); +} + +.users-list__link:hover, +.users-list__link:focus { + background: hsl(219, 64%, 35%); + color: #fff; +} + +.users-list__link:active { + text-decoration: none; +} + +.users-list__link.active { + background: hsl(219, 64%, 25%); + color: #fff; + text-decoration: none; +} + +.user-info { + flex: 1 1 60ch; + padding: 1rem; +} + +.user-info__title { + margin-bottom: 1rem; +} + +.user-info__label { + font-weight: 600; + margin-top: 1rem; +} + +.copyright { + font-size: 0.9rem; +} diff --git a/public/projects/js-small-apps/yarn.lock b/public/projects/js-small-apps/yarn.lock new file mode 100644 index 0000000..b3bfcdd --- /dev/null +++ b/public/projects/js-small-apps/yarn.lock @@ -0,0 +1,1795 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.15.8" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503" + integrity sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg== + dependencies: + "@babel/highlight" "^7.14.5" + +"@babel/helper-validator-identifier@^7.14.5": + version "7.15.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + +"@babel/highlight@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@commitlint/cli@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-16.2.1.tgz#ca4e557829a2755f0e1f0cd69b56b83ce2510173" + integrity sha512-zfKf+B9osuiDbxGMJ7bWFv7XFCW8wlQYPtCffNp7Ukdb7mdrep5R9e03vPUZysnwp8NX6hg05kPEvnD/wRIGWw== + dependencies: + "@commitlint/format" "^16.2.1" + "@commitlint/lint" "^16.2.1" + "@commitlint/load" "^16.2.1" + "@commitlint/read" "^16.2.1" + "@commitlint/types" "^16.2.1" + lodash "^4.17.19" + resolve-from "5.0.0" + resolve-global "1.0.0" + yargs "^17.0.0" + +"@commitlint/config-conventional@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-16.2.1.tgz#2cf47b505fb259777c063538c8498d8fd9b47779" + integrity sha512-cP9gArx7gnaj4IqmtCIcHdRjTYdRUi6lmGE+lOzGGjGe45qGOS8nyQQNvkNy2Ey2VqoSWuXXkD8zCUh6EHf1Ww== + dependencies: + conventional-changelog-conventionalcommits "^4.3.1" + +"@commitlint/config-validator@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/config-validator/-/config-validator-16.2.1.tgz#794e769afd4756e4cf1bfd823b6612932e39c56d" + integrity sha512-hogSe0WGg7CKmp4IfNbdNES3Rq3UEI4XRPB8JL4EPgo/ORq5nrGTVzxJh78omibNuB8Ho4501Czb1Er1MoDWpw== + dependencies: + "@commitlint/types" "^16.2.1" + ajv "^6.12.6" + +"@commitlint/ensure@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/ensure/-/ensure-16.2.1.tgz#0fc538173f95c1eb2694eeedb79cab478347f16f" + integrity sha512-/h+lBTgf1r5fhbDNHOViLuej38i3rZqTQnBTk+xEg+ehOwQDXUuissQ5GsYXXqI5uGy+261ew++sT4EA3uBJ+A== + dependencies: + "@commitlint/types" "^16.2.1" + lodash "^4.17.19" + +"@commitlint/execute-rule@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/execute-rule/-/execute-rule-16.2.1.tgz#60be73be4b9af97a41546e7ce59fdd33787c65f8" + integrity sha512-oSls82fmUTLM6cl5V3epdVo4gHhbmBFvCvQGHBRdQ50H/690Uq1Dyd7hXMuKITCIdcnr9umyDkr8r5C6HZDF3g== + +"@commitlint/format@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/format/-/format-16.2.1.tgz#6e673f710c799be78e68b2682323e04f75080d07" + integrity sha512-Yyio9bdHWmNDRlEJrxHKglamIk3d6hC0NkEUW6Ti6ipEh2g0BAhy8Od6t4vLhdZRa1I2n+gY13foy+tUgk0i1Q== + dependencies: + "@commitlint/types" "^16.2.1" + chalk "^4.0.0" + +"@commitlint/is-ignored@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/is-ignored/-/is-ignored-16.2.1.tgz#cc688ec73a3d204b90f8086821a08814da461e5e" + integrity sha512-exl8HRzTIfb1YvDJp2b2HU5z1BT+9tmgxR2XF0YEzkMiCIuEKh+XLeocPr1VcvAKXv3Cmv5X/OfNRp+i+/HIhQ== + dependencies: + "@commitlint/types" "^16.2.1" + semver "7.3.5" + +"@commitlint/lint@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-16.2.1.tgz#c773f082cd4f69cb7807b805b691d2a52c732f97" + integrity sha512-fNINQ3X2ZqsCkNB3Z0Z8ElmhewqrS3gy2wgBTx97BkcjOWiyPAGwDJ752hwrsUnWAVBRztgw826n37xPzxsOgg== + dependencies: + "@commitlint/is-ignored" "^16.2.1" + "@commitlint/parse" "^16.2.1" + "@commitlint/rules" "^16.2.1" + "@commitlint/types" "^16.2.1" + +"@commitlint/load@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-16.2.1.tgz#301bda1bff66b3e40a85819f854eda72538d8e24" + integrity sha512-oSpz0jTyVI/A1AIImxJINTLDOMB8YF7lWGm+Jg5wVWM0r7ucpuhyViVvpSRTgvL0z09oIxlctyFGWUQQpI42uw== + dependencies: + "@commitlint/config-validator" "^16.2.1" + "@commitlint/execute-rule" "^16.2.1" + "@commitlint/resolve-extends" "^16.2.1" + "@commitlint/types" "^16.2.1" + "@types/node" ">=12" + chalk "^4.0.0" + cosmiconfig "^7.0.0" + cosmiconfig-typescript-loader "^1.0.0" + lodash "^4.17.19" + resolve-from "^5.0.0" + typescript "^4.4.3" + +"@commitlint/message@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/message/-/message-16.2.1.tgz#bc6a0fa446a746ac2ca78cf372e4cec48daf620d" + integrity sha512-2eWX/47rftViYg7a3axYDdrgwKv32mxbycBJT6OQY/MJM7SUfYNYYvbMFOQFaA4xIVZt7t2Alyqslbl6blVwWw== + +"@commitlint/parse@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/parse/-/parse-16.2.1.tgz#50b359cb711ec566d2ee236a8e4c6baca07b77c0" + integrity sha512-2NP2dDQNL378VZYioLrgGVZhWdnJO4nAxQl5LXwYb08nEcN+cgxHN1dJV8OLJ5uxlGJtDeR8UZZ1mnQ1gSAD/g== + dependencies: + "@commitlint/types" "^16.2.1" + conventional-changelog-angular "^5.0.11" + conventional-commits-parser "^3.2.2" + +"@commitlint/read@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-16.2.1.tgz#e0539205d77cdb6879b560f95e5fb251e0c6f562" + integrity sha512-tViXGuaxLTrw2r7PiYMQOFA2fueZxnnt0lkOWqKyxT+n2XdEMGYcI9ID5ndJKXnfPGPppD0w/IItKsIXlZ+alw== + dependencies: + "@commitlint/top-level" "^16.2.1" + "@commitlint/types" "^16.2.1" + fs-extra "^10.0.0" + git-raw-commits "^2.0.0" + +"@commitlint/resolve-extends@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-16.2.1.tgz#2f7833a5a3a7aa79f508e59fcb0f1d33c45ed360" + integrity sha512-NbbCMPKTFf2J805kwfP9EO+vV+XvnaHRcBy6ud5dF35dxMsvdJqke54W3XazXF1ZAxC4a3LBy4i/GNVBAthsEg== + dependencies: + "@commitlint/config-validator" "^16.2.1" + "@commitlint/types" "^16.2.1" + import-fresh "^3.0.0" + lodash "^4.17.19" + resolve-from "^5.0.0" + resolve-global "^1.0.0" + +"@commitlint/rules@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-16.2.1.tgz#7264aa1c754e1c212aeceb27e5eb380cfa7bb233" + integrity sha512-ZFezJXQaBBso+BOTre/+1dGCuCzlWVaeLiVRGypI53qVgPMzQqZhkCcrxBFeqB87qeyzr4A4EoG++IvITwwpIw== + dependencies: + "@commitlint/ensure" "^16.2.1" + "@commitlint/message" "^16.2.1" + "@commitlint/to-lines" "^16.2.1" + "@commitlint/types" "^16.2.1" + execa "^5.0.0" + +"@commitlint/to-lines@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/to-lines/-/to-lines-16.2.1.tgz#42d000f34dc0406f514991e86237fdab5e8affd0" + integrity sha512-9/VjpYj5j1QeY3eiog1zQWY6axsdWAc0AonUUfyZ7B0MVcRI0R56YsHAfzF6uK/g/WwPZaoe4Lb1QCyDVnpVaQ== + +"@commitlint/top-level@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/top-level/-/top-level-16.2.1.tgz#bdaa53ab3d8970e0288879f1a342a8c2dfe01583" + integrity sha512-lS6GSieHW9y6ePL73ied71Z9bOKyK+Ib9hTkRsB8oZFAyQZcyRwq2w6nIa6Fngir1QW51oKzzaXfJL94qwImyw== + dependencies: + find-up "^5.0.0" + +"@commitlint/types@^16.2.1": + version "16.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-16.2.1.tgz#f25d373b88b01e51fc3fa44488101361945a61bd" + integrity sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA== + dependencies: + chalk "^4.0.0" + +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + +"@types/minimist@^1.2.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + +"@types/node@>=12": + version "17.0.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074" + integrity sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA== + +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +JSONStream@^1.0.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + +ajv@^6.12.6: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +array-ify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" + integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^2.0.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +compare-func@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" + integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== + dependencies: + array-ify "^1.0.0" + dot-prop "^5.1.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + +conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12: + version "5.0.13" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" + integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== + dependencies: + compare-func "^2.0.0" + q "^1.5.1" + +conventional-changelog-atom@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz#a759ec61c22d1c1196925fca88fe3ae89fd7d8de" + integrity sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw== + dependencies: + q "^1.5.1" + +conventional-changelog-codemirror@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz#398e9530f08ce34ec4640af98eeaf3022eb1f7dc" + integrity sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw== + dependencies: + q "^1.5.1" + +conventional-changelog-config-spec@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz#874a635287ef8b581fd8558532bf655d4fb59f2d" + integrity sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ== + +conventional-changelog-conventionalcommits@4.6.1, conventional-changelog-conventionalcommits@^4.3.1, conventional-changelog-conventionalcommits@^4.5.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.1.tgz#f4c0921937050674e578dc7875f908351ccf4014" + integrity sha512-lzWJpPZhbM1R0PIzkwzGBCnAkH5RKJzJfFQZcl/D+2lsJxAwGnDKBqn/F4C1RD31GJNn8NuKWQzAZDAVXPp2Mw== + dependencies: + compare-func "^2.0.0" + lodash "^4.17.15" + q "^1.5.1" + +conventional-changelog-core@^4.2.1: + version "4.2.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" + integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== + dependencies: + add-stream "^1.0.0" + conventional-changelog-writer "^5.0.0" + conventional-commits-parser "^3.2.0" + dateformat "^3.0.0" + get-pkg-repo "^4.0.0" + git-raw-commits "^2.0.8" + git-remote-origin-url "^2.0.0" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" + q "^1.5.1" + read-pkg "^3.0.0" + read-pkg-up "^3.0.0" + through2 "^4.0.0" + +conventional-changelog-ember@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz#619b37ec708be9e74a220f4dcf79212ae1c92962" + integrity sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A== + dependencies: + q "^1.5.1" + +conventional-changelog-eslint@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz#689bd0a470e02f7baafe21a495880deea18b7cdb" + integrity sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA== + dependencies: + q "^1.5.1" + +conventional-changelog-express@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz#420c9d92a347b72a91544750bffa9387665a6ee8" + integrity sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ== + dependencies: + q "^1.5.1" + +conventional-changelog-jquery@^3.0.11: + version "3.0.11" + resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz#d142207400f51c9e5bb588596598e24bba8994bf" + integrity sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw== + dependencies: + q "^1.5.1" + +conventional-changelog-jshint@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz#f2d7f23e6acd4927a238555d92c09b50fe3852ff" + integrity sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA== + dependencies: + compare-func "^2.0.0" + q "^1.5.1" + +conventional-changelog-preset-loader@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" + integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== + +conventional-changelog-writer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz#c4042f3f1542f2f41d7d2e0d6cad23aba8df8eec" + integrity sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g== + dependencies: + conventional-commits-filter "^2.0.7" + dateformat "^3.0.0" + handlebars "^4.7.6" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + meow "^8.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^4.0.0" + +conventional-changelog@3.1.24: + version "3.1.24" + resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464" + integrity sha512-ed6k8PO00UVvhExYohroVPXcOJ/K1N0/drJHx/faTH37OIZthlecuLIRX/T6uOp682CAoVoFpu+sSEaeuH6Asg== + dependencies: + conventional-changelog-angular "^5.0.12" + conventional-changelog-atom "^2.0.8" + conventional-changelog-codemirror "^2.0.8" + conventional-changelog-conventionalcommits "^4.5.0" + conventional-changelog-core "^4.2.1" + conventional-changelog-ember "^2.0.9" + conventional-changelog-eslint "^3.0.9" + conventional-changelog-express "^2.0.6" + conventional-changelog-jquery "^3.0.11" + conventional-changelog-jshint "^2.0.9" + conventional-changelog-preset-loader "^2.3.4" + +conventional-commits-filter@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" + integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== + dependencies: + lodash.ismatch "^4.4.0" + modify-values "^1.0.0" + +conventional-commits-parser@^3.2.0, conventional-commits-parser@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.3.tgz#fc43704698239451e3ef35fd1d8ed644f46bd86e" + integrity sha512-YyRDR7On9H07ICFpRm/igcdjIqebXbvf4Cff+Pf0BrBys1i1EOzx9iFXNlAbdrLAR8jf7bkUYkDAr8pEy0q4Pw== + dependencies: + JSONStream "^1.0.4" + is-text-path "^1.0.1" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +conventional-recommended-bump@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" + integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== + dependencies: + concat-stream "^2.0.0" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" + git-raw-commits "^2.0.8" + git-semver-tags "^4.1.1" + meow "^8.0.0" + q "^1.5.1" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig-typescript-loader@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.5.tgz#22373003194a1887bbccbdfd05a13501397109a8" + integrity sha512-FL/YR1nb8hyN0bAcP3MBaIoZravfZtVsN/RuPnoo6UVjqIrDxSNIpXHCGgJe0ZWy5yImpyD6jq5wCJ5f1nUv8g== + dependencies: + cosmiconfig "^7" + ts-node "^10.5.0" + +cosmiconfig@^7, cosmiconfig@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== + +dateformat@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + +decamelize-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +detect-newline@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dot-prop@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dotgitignore@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" + integrity sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA== + dependencies: + find-up "^3.0.0" + minimatch "^3.0.4" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +figures@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +fs-access@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" + integrity sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o= + dependencies: + null-check "^1.0.0" + +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-pkg-repo@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" + integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== + dependencies: + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" + through2 "^2.0.0" + yargs "^16.2.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +git-raw-commits@^2.0.0, git-raw-commits@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" + integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== + dependencies: + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +git-remote-origin-url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" + integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= + dependencies: + gitconfiglocal "^1.0.0" + pify "^2.3.0" + +git-semver-tags@^4.0.0, git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== + dependencies: + meow "^8.0.0" + semver "^6.0.0" + +gitconfiglocal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" + integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= + dependencies: + ini "^1.3.2" + +global-dirs@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= + dependencies: + ini "^1.3.4" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + +handlebars@^4.7.6: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" + integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== + dependencies: + lru-cache "^6.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +husky@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" + integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.2, ini@^1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-core-module@^2.2.0, is-core-module@^2.5.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" + integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== + dependencies: + has "^1.0.3" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-text-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" + integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= + dependencies: + text-extensions "^1.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + +kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.ismatch@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" + integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= + +lodash@^4.17.15, lodash@^4.17.19: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-obj@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== + +meow@^8.0.0: + version "8.1.2" + resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" + integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +modify-values@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" + integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== + +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +null-check@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" + integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0= + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +resolve-from@5.0.0, resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-global@1.0.0, resolve-global@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255" + integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== + dependencies: + global-dirs "^0.1.1" + +resolve@^1.10.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"semver@2 || 3 || 4 || 5": + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@7.3.5, semver@^7.1.1, semver@^7.3.4: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +semver@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.3: + version "3.0.5" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" + integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.10" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b" + integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA== + +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + +split@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +standard-version@^9.3.1: + version "9.3.2" + resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.3.2.tgz#28db8c1be66fd2d736f28f7c5de7619e64cd6dab" + integrity sha512-u1rfKP4o4ew7Yjbfycv80aNMN2feTiqseAhUhrrx2XtdQGmu7gucpziXe68Z4YfHVqlxVEzo4aUA0Iu3VQOTgQ== + dependencies: + chalk "^2.4.2" + conventional-changelog "3.1.24" + conventional-changelog-config-spec "2.1.0" + conventional-changelog-conventionalcommits "4.6.1" + conventional-recommended-bump "6.1.0" + detect-indent "^6.0.0" + detect-newline "^3.1.0" + dotgitignore "^2.1.0" + figures "^3.1.0" + find-up "^5.0.0" + fs-access "^1.0.1" + git-semver-tags "^4.0.0" + semver "^7.1.1" + stringify-package "^1.0.1" + yargs "^16.0.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringify-package@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" + integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-extensions@^1.0.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" + integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + +through@2, "through@>=2.2.7 <3": + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +trim-newlines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== + +ts-node@^10.5.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9" + integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" + yn "3.1.1" + +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typescript@^4.4.3: + version "4.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" + integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== + +uglify-js@^3.1.4: + version "3.14.2" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.2.tgz#d7dd6a46ca57214f54a2d0a43cad0f35db82ac99" + integrity sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@^20.2.2, yargs-parser@^20.2.3: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.0.0, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.0.0: + version "17.2.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea" + integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== |
