├── .devcontainer └── devcontainer.json ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── 01_02b └── script.js ├── 01_02e └── script.js ├── 01_03b └── script.js ├── 01_03e └── script.js ├── 02_02b └── script.js ├── 02_02e └── script.js ├── 02_03b └── script.js ├── 02_03e └── script.js ├── 03_02b └── script.js ├── 03_02e └── script.js ├── 03_03b └── script.js ├── 03_03e └── script.js ├── 04_02b └── script.js ├── 04_02e └── script.js ├── 04_03b └── script.js ├── 04_03e └── script.js ├── 05_02b └── script.js ├── 05_02e └── script.js ├── 05_03b └── script.js ├── 05_03e └── script.js ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── favicon.ico ├── package-lock.json ├── package.json └── solutions ├── 01_04_solution └── solution.js ├── 01_06_solution └── solution.js ├── 02_04_solution └── solution.js ├── 02_06_solution └── solution.js ├── 03_04_solution └── solution.js ├── 03_06_solution └── solution.js ├── 04_04_solution └── solution.js ├── 04_06_solution ├── explanation.md └── solution.js ├── 05_04_solution └── solution.js └── 05_06_solution └── solution.js /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | "GitHub.github-vscode-theme", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "ritwickdey.LiveServer", 7 | "stylelint.vscode-stylelint" 8 | // Additional Extensions Here 9 | ], 10 | "onCreateCommand": "echo PS1='\"$ \"' >> ~/.bashrc", //Set Terminal Prompt to $ 11 | "postAttachCommand": "git pull --all" 12 | } 13 | 14 | // DevContainer Reference: https://code.visualstudio.com/docs/remote/devcontainerjson-reference 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["eslint:recommended", "prettier"], 7 | overrides: [], 8 | parserOptions: { 9 | ecmaVersion: "latest", 10 | sourceType: "module", 11 | }, 12 | rules: {}, 13 | }; 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Codeowners for these exercise files: 2 | # * (asterisk) denotes "all files and folders" 3 | # Example: * @producer @instructor 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Issue Overview 9 | 10 | 11 | ## Describe your environment 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ## Expected Behavior 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | ## Possible Solution 28 | 29 | 30 | ## Screenshots / Video 31 | 32 | 33 | ## Related Issues 34 | 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Copy To Branches 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | copy-to-branches: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - name: Copy To Branches Action 12 | uses: planetoftheweb/copy-to-branches@v1.2 13 | env: 14 | key: main 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tmp 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.cursorBlinking": "solid", 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.fontFamily": "ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace", 6 | "editor.fontLigatures": false, 7 | "editor.fontSize": 22, 8 | "editor.formatOnPaste": true, 9 | "editor.formatOnSave": true, 10 | "editor.lineNumbers": "on", 11 | "editor.matchBrackets": "always", 12 | "editor.minimap.enabled": false, 13 | "editor.smoothScrolling": true, 14 | "editor.tabSize": 2, 15 | "editor.useTabStops": true, 16 | "emmet.triggerExpansionOnTab": true, 17 | "explorer.openEditors.visible": 0, 18 | "files.autoSave": "afterDelay", 19 | "screencastMode.onlyKeyboardShortcuts": true, 20 | "terminal.integrated.fontSize": 18, 21 | "workbench.activityBar.visible": true, 22 | "workbench.colorTheme": "Visual Studio Dark", 23 | "workbench.fontAliasing": "antialiased", 24 | "workbench.statusBar.visible": true, 25 | "liveServer.settings.root": "/docs", 26 | "prettier.enable": true, 27 | "eslint.alwaysShowStatus": false, 28 | "liveServer.settings.donotVerifyTags": true 29 | } 30 | -------------------------------------------------------------------------------- /01_02b/script.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { id: 1, parent: 0, text: "Top-level comment 1" }, 3 | { id: 2, parent: 0, text: "Top-level comment 2" }, 4 | { id: 3, parent: 1, text: "Reply to Top-level comment 1" }, 5 | { id: 4, parent: 3, text: "Reply to Reply to Top-level comment 1" }, 6 | ]; 7 | 8 | /** 9 | * Restructure the flat data array into a nested array. 10 | * 11 | * @param {array} data 12 | * @returns {array} 13 | */ 14 | function restructureArray(data) { 15 | // Create an array to hold the root elements 16 | const root = []; 17 | 18 | return root; 19 | } 20 | 21 | const result = restructureArray(data); 22 | 23 | // Output the resut array as a tree. 24 | console.log(JSON.stringify(result, null, 2)); 25 | -------------------------------------------------------------------------------- /01_02e/script.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { id: 1, parent: 0, text: "Top-level comment 1" }, 3 | { id: 2, parent: 0, text: "Top-level comment 2" }, 4 | { id: 3, parent: 1, text: "Reply to Top-level comment 1" }, 5 | { id: 4, parent: 3, text: "Reply to Reply to Top-level comment 1" }, 6 | ]; 7 | 8 | /** 9 | * Restructure the flat data array into a nested array. 10 | * 11 | * @param {array} data 12 | * @returns {array} 13 | */ 14 | function restructureArray(data) { 15 | // Create a map of the data array 16 | const dataMap = {}; 17 | // Create an array to hold the root elements 18 | const root = []; 19 | 20 | // Iterate through the data array and add each item to the map 21 | // with its ID as the key and add an empty children array. 22 | data.forEach((item) => { 23 | dataMap[item.id] = { 24 | ...item, 25 | children: [], 26 | }; 27 | }); 28 | 29 | // Iterate through the data array again. If the item has a parent, 30 | // add it as a child of its parent. If it doesn't have a parent, 31 | // it's a root element and should be added to the `root` array. 32 | data.forEach((item) => { 33 | const parent = dataMap[item.parent]; 34 | if (parent) { 35 | parent.children.push(dataMap[item.id]); 36 | } else { 37 | root.push(dataMap[item.id]); 38 | } 39 | }); 40 | 41 | return root; 42 | } 43 | 44 | const result = restructureArray(data); 45 | 46 | // Output the resut array as a tree. 47 | console.log(JSON.stringify(result, null, 2)); 48 | -------------------------------------------------------------------------------- /01_03b/script.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { id: 1, parent: 0, text: "Top-level comment 1" }, 3 | { id: 2, parent: 0, text: "Top-level comment 2" }, 4 | { id: 3, parent: 1, text: "Reply to Top-level comment 1" }, 5 | { id: 4, parent: 3, text: "Reply to Reply to Top-level comment 1" }, 6 | ]; 7 | 8 | /** 9 | * Restructure the flat data array into a nested array. 10 | * 11 | * @param {array} data 12 | * @returns {array} 13 | */ 14 | function restructureArray(data) { 15 | // Create a map of the data array 16 | const dataMap = {}; 17 | // Create an array to hold the root elements 18 | const root = []; 19 | 20 | // Iterate through the data array and add each item to the map 21 | // with its ID as the key and add an empty children array. 22 | data.forEach((item) => { 23 | dataMap[item.id] = { 24 | ...item, 25 | children: [], 26 | }; 27 | }); 28 | 29 | // Iterate through the data array again. If the item has a parent, 30 | // add it as a child of its parent. If it doesn't have a parent, 31 | // it's a root element and should be added to the `root` array. 32 | data.forEach((item) => { 33 | const parent = dataMap[item.parent]; 34 | if (parent) { 35 | parent.children.push(dataMap[item.id]); 36 | } else { 37 | root.push(dataMap[item.id]); 38 | } 39 | }); 40 | 41 | return root; 42 | } 43 | 44 | const comments = restructureArray(data); 45 | console.log(JSON.stringify(comments, null, 2)); 46 | 47 | /** 48 | * Generate a nested text string from the nested array. 49 | * @param {*} comments 50 | * @param {*} level 51 | * @returns {string} 52 | */ 53 | function generateNestedText(comments, level = 0) { 54 | let output = ""; 55 | 56 | return output; 57 | } 58 | 59 | const result = generateNestedText(comments); 60 | 61 | console.log(result); 62 | -------------------------------------------------------------------------------- /01_03e/script.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { id: 1, parent: 0, text: "Top-level comment 1" }, 3 | { id: 2, parent: 0, text: "Top-level comment 2" }, 4 | { id: 3, parent: 1, text: "Reply to Top-level comment 1" }, 5 | { id: 4, parent: 3, text: "Reply to Reply to Top-level comment 1" }, 6 | ]; 7 | 8 | /** 9 | * Restructure the flat data array into a nested array. 10 | * 11 | * @param {array} data 12 | * @returns {array} 13 | */ 14 | function restructureArray(data) { 15 | // Create a map of the data array 16 | const dataMap = {}; 17 | // Create an array to hold the root elements 18 | const root = []; 19 | 20 | // Iterate through the data array and add each item to the map 21 | // with its ID as the key and add an empty children array. 22 | data.forEach((item) => { 23 | dataMap[item.id] = { 24 | ...item, 25 | children: [], 26 | }; 27 | }); 28 | 29 | // Iterate through the data array again. If the item has a parent, 30 | // add it as a child of its parent. If it doesn't have a parent, 31 | // it's a root element and should be added to the `root` array. 32 | data.forEach((item) => { 33 | const parent = dataMap[item.parent]; 34 | if (parent) { 35 | parent.children.push(dataMap[item.id]); 36 | } else { 37 | root.push(dataMap[item.id]); 38 | } 39 | }); 40 | 41 | return root; 42 | } 43 | 44 | const comments = restructureArray(data); 45 | console.log(JSON.stringify(comments, null, 2)); 46 | 47 | /** 48 | * Generate a nested text string from the nested array. 49 | * @param {*} comments 50 | * @param {*} level 51 | * @returns {string} 52 | */ 53 | function generateNestedText(comments, level = 0) { 54 | let output = ""; 55 | 56 | // Iterate through the comments array and add each comment's text 57 | comments.forEach((comment) => { 58 | // Create an indent string based on the current level 59 | let indent = "-".repeat(level + 1) + " "; 60 | 61 | // Add the comment text to the output string 62 | output += indent + comment.text + "\n"; 63 | 64 | // If the comment has children, recursively call this function 65 | if (comment.children && comment.children.length > 0) { 66 | output += generateNestedText(comment.children, level + 1); 67 | } 68 | }); 69 | 70 | return output; 71 | } 72 | 73 | const result = generateNestedText(comments); 74 | 75 | console.log(result); 76 | -------------------------------------------------------------------------------- /02_02b/script.js: -------------------------------------------------------------------------------- 1 | // Vehicle class 2 | class Vehicle {} 3 | 4 | const myCar = new Car("My Car", 4); 5 | console.log(myCar.getDescription()); // My Car has 4 wheels and 4 doors 6 | 7 | const myBike = new Bike("My Bike", "offroad"); 8 | console.log(myBike.getDescription()); // My Bike has 2 wheels and is a offroad type 9 | -------------------------------------------------------------------------------- /02_02e/script.js: -------------------------------------------------------------------------------- 1 | // Vehicle class 2 | class Vehicle { 3 | constructor(name, wheels) { 4 | this.name = name; 5 | this.wheels = wheels; 6 | } 7 | 8 | // Return a description of the vehicle 9 | getDescription() { 10 | return `${this.name} has ${this.wheels} wheels`; 11 | } 12 | } 13 | 14 | // Car class extends Vehicle 15 | // Add number of doors 16 | class Car extends Vehicle { 17 | constructor(name, doors) { 18 | super(name, 4); 19 | this.doors = doors; 20 | } 21 | } 22 | 23 | // Bike class extends Vehicle 24 | class Bike extends Vehicle { 25 | constructor(name, type) { 26 | super(name, 2); 27 | this.type = type; 28 | } 29 | } 30 | 31 | const myCar = new Car("My Car", 4); 32 | console.log(myCar.getDescription()); // My Car has 4 wheels 33 | console.log(myCar.doors); // 4 34 | 35 | const myBike = new Bike("My Bike", "offroad"); 36 | console.log(myBike.getDescription()); // My Bike has 2 wheels 37 | console.log(myBike.type); // offroad 38 | -------------------------------------------------------------------------------- /02_03b/script.js: -------------------------------------------------------------------------------- 1 | // Vehicle class 2 | class Vehicle { 3 | constructor(name, wheels) { 4 | this.name = name; 5 | this.wheels = wheels; 6 | } 7 | 8 | // Return a description of the vehicle 9 | getDescription() { 10 | return `${this.name} has ${this.wheels} wheels`; 11 | } 12 | } 13 | 14 | // Car class extends Vehicle 15 | // Add number of doors 16 | class Car extends Vehicle { 17 | constructor(name, doors) { 18 | super(name, 4); 19 | this.doors = doors; 20 | } 21 | } 22 | 23 | // Bike class extends Vehicle 24 | class Bike extends Vehicle { 25 | constructor(name, type) { 26 | super(name, 2); 27 | this.type = type; 28 | } 29 | } 30 | 31 | const myCar = new Car("My Car", 4); 32 | console.log(myCar.getDescription()); // My Car has 4 wheels 33 | console.log(myCar.doors); // 4 34 | 35 | const myBike = new Bike("My Bike", "offroad"); 36 | console.log(myBike.getDescription()); // My Bike has 2 wheels 37 | console.log(myBike.type); // offroad 38 | -------------------------------------------------------------------------------- /02_03e/script.js: -------------------------------------------------------------------------------- 1 | // Vehicle class 2 | class Vehicle { 3 | constructor(name, wheels) { 4 | this.name = name; 5 | this.wheels = wheels; 6 | } 7 | 8 | // Return a description of the vehicle 9 | getDescription() { 10 | return `${this.name} has ${this.wheels} wheels`; 11 | } 12 | } 13 | 14 | // Car class extends Vehicle 15 | // Add number of doors 16 | class Car extends Vehicle { 17 | constructor(name, wheels, doors) { 18 | super(name, wheels); 19 | this.doors = doors; 20 | } 21 | 22 | // Override the getDescription method 23 | // Inherit the original method with super 24 | getDescription() { 25 | return `${super.getDescription()} and ${this.doors} doors`; 26 | } 27 | } 28 | 29 | // Bike class extends Vehicle 30 | class Bike extends Vehicle { 31 | constructor(name, wheels, type) { 32 | super(name, wheels); 33 | this.type = type; 34 | } 35 | 36 | // Override the getDescription method 37 | // Inherit the original method with super 38 | getDescription() { 39 | return `${super.getDescription()} and is a ${this.type} type`; 40 | } 41 | } 42 | 43 | const myCar = new Car("My Car", 4, 4); 44 | console.log(myCar.getDescription()); // My Car has 4 wheels and 4 doors 45 | 46 | const myBike = new Bike("My Bike", 3, "offroad"); 47 | console.log(myBike.getDescription()); // My Bike has 2 wheels and is a offroad type 48 | -------------------------------------------------------------------------------- /03_02b/script.js: -------------------------------------------------------------------------------- 1 | class UniqueClass { 2 | constructor(data) { 3 | this.data = data; 4 | } 5 | 6 | getData() { 7 | return this.data; 8 | } 9 | } 10 | 11 | const instance1 = new UniqueClass( 12 | "Data for the first instance of the UniqueClass." 13 | ); 14 | 15 | const instance2 = new UniqueClass( 16 | "The second instance of the UniqueClass should not be set." 17 | ); 18 | 19 | console.log(instance1.getData()); // "Data for the UniqueClass" 20 | console.log(instance2.getData()); // "Data for the UniqueClass" 21 | -------------------------------------------------------------------------------- /03_02e/script.js: -------------------------------------------------------------------------------- 1 | class UniqueClass { 2 | constructor(data) { 3 | if (UniqueClass.instance) { 4 | return UniqueClass.instance; 5 | } 6 | UniqueClass.instance = this; 7 | this.data = data; 8 | } 9 | 10 | getData() { 11 | return this.data; 12 | } 13 | } 14 | 15 | const instance1 = new UniqueClass( 16 | "Data for the first instance of the UniqueClass." 17 | ); 18 | const instance2 = new UniqueClass( 19 | "The second instance of the UniqueClass should not be set." 20 | ); 21 | 22 | // Freeze the instance to prevent changes. 23 | Object.freeze(instance1); 24 | 25 | instance1.data = "something new"; // This will not change the data. 26 | 27 | console.log(instance1.getData()); // "Data for the UniqueClass" 28 | console.log(instance2.getData()); // "Data for the UniqueClass" 29 | -------------------------------------------------------------------------------- /03_03b/script.js: -------------------------------------------------------------------------------- 1 | class Booking { 2 | constructor() { 3 | this._time = null; 4 | } 5 | 6 | set time(value) { 7 | let bookingHour = new Date(value).getHours(); 8 | if (bookingHour < 9 || bookingHour > 16) { 9 | throw new Error("Bookings can only be made between 9AM and 4PM"); 10 | } 11 | this._time = new Date(value); 12 | } 13 | 14 | get time() { 15 | return this._time; 16 | } 17 | } 18 | 19 | let booking = new Booking(); 20 | 21 | let bookingAttempts = [ 22 | new Date("2023-05-24T10:00:00"), 23 | new Date("2023-05-24T08:00:00"), 24 | new Date("2023-05-24T17:00:00"), 25 | ]; 26 | 27 | for (let attempt of bookingAttempts) { 28 | try { 29 | booking.time = attempt; 30 | console.log(`Booking made successfully at ${booking._time}`); 31 | } catch (error) { 32 | console.log(`Booking attempt failed for ${attempt}: ${error.message}`); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /03_03e/script.js: -------------------------------------------------------------------------------- 1 | class Booking { 2 | constructor() { 3 | this._time = null; 4 | } 5 | } 6 | 7 | class Scheduler { 8 | constructor() { 9 | this._time = null; 10 | } 11 | } 12 | let validator = { 13 | set: function (obj, prop, value) { 14 | if (prop === "_time") { 15 | let bookingHour = new Date(value).getHours(); 16 | if (bookingHour < 9 || bookingHour > 16) { 17 | throw new Error("Bookings can only be made between 9AM and 4PM"); 18 | } 19 | } 20 | 21 | // The default behavior to store the value 22 | obj[prop] = new Date(value); 23 | 24 | // Indicate success 25 | return true; 26 | }, 27 | }; 28 | 29 | let booking = new Proxy(new Booking(), validator); 30 | 31 | let bookingAttempts = [ 32 | new Date("2023-05-24T10:00:00"), 33 | new Date("2023-05-24T08:00:00"), 34 | new Date("2023-05-24T17:00:00"), 35 | ]; 36 | 37 | for (let attempt of bookingAttempts) { 38 | try { 39 | booking._time = attempt; 40 | console.log(`Booking made successfully at ${booking._time}`); 41 | } catch (error) { 42 | console.log(`Booking attempt failed for ${attempt}: ${error.message}`); 43 | } 44 | } 45 | 46 | let appointment = new Proxy(new Scheduler(), validator); 47 | 48 | appointment._time = new Date("2023-05-24T10:00:00"); 49 | console.log(`Appointment made successfully at ${appointment._time}`); 50 | appointment._time = new Date("2023-05-24T08:00:00"); 51 | console.log(`Appointment made successfully at ${appointment._time}`); 52 | -------------------------------------------------------------------------------- /04_02b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Observer pattern 3 | * - A one-to-many dependency between objects. 4 | * - When one object changes state, all its dependents are notified and updated automatically. 5 | * - Also known as Publish-Subscribe pattern 6 | 7 | */ 8 | class Product { 9 | constructor(name) { 10 | this.name = name; 11 | this.observers = []; 12 | } 13 | 14 | // The method that notifies all subscribers. 15 | backInStock() { 16 | // Define the product message. 17 | this.notifyObservers(`${this.name} is back in stock.`); 18 | } 19 | } 20 | 21 | class Customer { 22 | constructor(name) { 23 | this.name = name; 24 | } 25 | 26 | // The method that is called when the customer is notified. 27 | message(productMessage) { 28 | console.log(`${this.name}, ${productMessage}`); 29 | } 30 | } 31 | 32 | // A product that is out of stock 33 | let backpack = new Product("Green Frog Pack"); 34 | 35 | // Some customers 36 | let simran = new Customer("Simran"); 37 | let maiken = new Customer("Maiken"); 38 | 39 | backpack.registerObserver(simran); 40 | backpack.registerObserver(maiken); 41 | 42 | // Notify all customers when backpack is back in stock 43 | backpack.backInStock(); 44 | -------------------------------------------------------------------------------- /04_02e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Observer pattern 3 | * - A one-to-many dependency between objects. 4 | * - When one object changes state, all its dependents are notified and updated automatically. 5 | * - Also known as Publish-Subscribe pattern 6 | 7 | */ 8 | class Product { 9 | constructor(name) { 10 | this.name = name; 11 | this.observers = []; 12 | } 13 | 14 | // The method that notifies all subscribers. 15 | backInStock() { 16 | // Define the product message. 17 | this.notifyObservers(`${this.name} is back in stock.`); 18 | } 19 | 20 | // The method that adds a new subscriber. 21 | registerObserver(customer) { 22 | this.observers.push(customer.message.bind(customer)); 23 | } 24 | 25 | // The method that calls all subscribers. 26 | notifyObservers(productMessage) { 27 | this.observers.forEach((customer) => customer(productMessage)); 28 | } 29 | } 30 | 31 | class Customer { 32 | constructor(name) { 33 | this.name = name; 34 | } 35 | 36 | // The method that is called when the customer is notified. 37 | message(productMessage) { 38 | console.log(`${this.name}, ${productMessage}`); 39 | } 40 | } 41 | 42 | // A product that is out of stock 43 | let backpack = new Product("Green Frog Pack"); 44 | 45 | // Some customers 46 | let simran = new Customer("Simran"); 47 | let maiken = new Customer("Maiken"); 48 | 49 | backpack.registerObserver(simran); 50 | backpack.registerObserver(maiken); 51 | 52 | // Notify all customers when backpack is back in stock 53 | backpack.backInStock(); 54 | -------------------------------------------------------------------------------- /04_03b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Observer pattern 3 | * - A one-to-many dependency between objects. 4 | * - When one object changes state, all its dependents are notified and updated automatically. 5 | * - Also known as Publish-Subscribe pattern 6 | 7 | */ 8 | class Product { 9 | constructor(name) { 10 | this.name = name; 11 | this.observers = []; 12 | } 13 | 14 | // The method that notifies all subscribers. 15 | backInStock() { 16 | // Define the product message. 17 | this.notifyObservers(`${this.name} is back in stock.`); 18 | } 19 | 20 | // The method that adds a new subscriber. 21 | registerObserver(customer) { 22 | this.observers.push(customer.message.bind(customer)); 23 | } 24 | 25 | // The method that calls all subscribers. 26 | notifyObservers(productMessage) { 27 | this.observers.forEach((customer) => customer(productMessage)); 28 | } 29 | } 30 | 31 | class Customer { 32 | constructor(name) { 33 | this.name = name; 34 | } 35 | 36 | // The method that is called when the customer is notified. 37 | message(productMessage) { 38 | console.log(`${this.name}, ${productMessage}`); 39 | } 40 | } 41 | 42 | // A product that is out of stock 43 | let backpack = new Product("Green Frog Pack"); 44 | 45 | // Some customers 46 | let simran = new Customer("Simran"); 47 | let maiken = new Customer("Maiken"); 48 | 49 | backpack.registerObserver(simran); 50 | backpack.registerObserver(maiken); 51 | 52 | // Notify all customers when backpack is back in stock 53 | backpack.backInStock(); 54 | 55 | // Remove a customer from the observer list 56 | backpack.removeObserver(simran); 57 | -------------------------------------------------------------------------------- /04_03e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Observer pattern 3 | * - A one-to-many dependency between objects. 4 | * - When one object changes state, all its dependents are notified and updated automatically. 5 | * - Also known as Publish-Subscribe pattern 6 | 7 | */ 8 | class Product { 9 | constructor(name) { 10 | this.name = name; 11 | this.observers = []; 12 | } 13 | 14 | // The method that notifies all subscribers. 15 | backInStock() { 16 | this.notifyObservers(`${this.name} is back in stock.`); 17 | } 18 | 19 | // The method that adds a new subscriber. 20 | registerObserver(customer) { 21 | this.observers.push(customer.boundMessage); 22 | console.log(`Registered ${customer.name} as an observer of ${this.name}.`); 23 | } 24 | 25 | // The method that removes a subscriber. 26 | removeObserver(customer) { 27 | this.observers = this.observers.filter( 28 | (customer) => customer !== customer.boundMessage 29 | ); 30 | console.log(`Removed ${customer.name} from observers of ${this.name}.`); 31 | } 32 | 33 | // The method that calls all subscribers. 34 | notifyObservers(productMessage) { 35 | this.observers.forEach((customer) => customer(productMessage)); 36 | } 37 | } 38 | 39 | class Customer { 40 | constructor(name) { 41 | this.name = name; 42 | this.boundMessage = this.message.bind(this); 43 | } 44 | 45 | // The method that is called when the customer is notified. 46 | message(productMessage) { 47 | console.log(`${this.name}, ${productMessage}`); 48 | } 49 | } 50 | 51 | // A product that is out of stock 52 | let backpack = new Product("Green Frog Pack"); 53 | 54 | // Some customers 55 | let simran = new Customer("Simran"); 56 | let maiken = new Customer("Maiken"); 57 | 58 | backpack.registerObserver(simran); 59 | backpack.registerObserver(maiken); 60 | 61 | // Notify all customers when backpack is back in stock 62 | backpack.backInStock(); 63 | 64 | // Remove a customer from the observer list 65 | backpack.removeObserver(simran); 66 | -------------------------------------------------------------------------------- /05_02b/script.js: -------------------------------------------------------------------------------- 1 | // Reactive Object 2 | class ReactiveObject { 3 | constructor() { 4 | this.subscribers = []; 5 | } 6 | 7 | // Subscribe to changes 8 | subscribe(callback) { 9 | // Add callback to subscribers 10 | this.subscribers.push(callback); 11 | } 12 | 13 | // Unsubscribe from changes 14 | unsubscribe(callback) { 15 | // Remove the callback from subscribers 16 | this.subscribers = this.subscribers.filter((sub) => sub !== callback); 17 | } 18 | 19 | updateSubscribers() { 20 | this.subscribers.forEach((callback) => callback(this)); 21 | } 22 | } 23 | 24 | // Temperature Monitor Object 25 | class TemperatureMonitor extends ReactiveObject { 26 | constructor() { 27 | super(); 28 | this.temperature = 0; 29 | } 30 | 31 | // Set temperature 32 | setTemperature(newTemperature) { 33 | this.temperature = newTemperature; 34 | this.updateSubscribers(); 35 | } 36 | } 37 | 38 | // Now we can use this reactive object: 39 | const monitor = new TemperatureMonitor(); 40 | 41 | // Define a callback for temperature changes 42 | const alertTemperature = (monitor) => { 43 | if (monitor.temperature > 30) { 44 | console.log( 45 | `WARNING: It's ${monitor.temperature}°C. It's uncomfortably hot!` 46 | ); 47 | } 48 | }; 49 | 50 | // Subscribe to changes 51 | monitor.subscribe(alertTemperature); 52 | 53 | // Set a new temperature 54 | monitor.setTemperature(35); // Logs: 'WARNING: Temperature too high!' 55 | 56 | // Unsubscribe from changes 57 | monitor.unsubscribe(alertTemperature); 58 | 59 | // Set a new temperature 60 | monitor.setTemperature(40); // Does not log anything because we have unsubscribed. 61 | -------------------------------------------------------------------------------- /05_02e/script.js: -------------------------------------------------------------------------------- 1 | // Reactive Object 2 | class ReactiveObject { 3 | constructor() { 4 | this.subscribers = []; 5 | } 6 | 7 | // Subscribe to changes 8 | subscribe(callback) { 9 | // Add callback to subscribers 10 | this.subscribers.push(callback); 11 | } 12 | 13 | // Unsubscribe from changes 14 | unsubscribe(callback) { 15 | // Remove the callback from subscribers 16 | this.subscribers = this.subscribers.filter((sub) => sub !== callback); 17 | } 18 | 19 | updateSubscribers() { 20 | this.subscribers.forEach((callback) => callback(this)); 21 | } 22 | } 23 | 24 | // Temperature Monitor Object 25 | class TemperatureMonitor extends ReactiveObject { 26 | constructor() { 27 | super(); 28 | this.temperature = 0; 29 | } 30 | 31 | // Set temperature 32 | setTemperature(newTemperature) { 33 | this.temperature = newTemperature; 34 | this.updateSubscribers(); 35 | } 36 | } 37 | 38 | // Now we can use this reactive object: 39 | const monitor = new TemperatureMonitor(); 40 | 41 | // Define a callback for temperature changes 42 | const alertTemperature = (monitor) => { 43 | if (monitor.temperature > 30) { 44 | console.log( 45 | `WARNING: It's ${monitor.temperature}°C. It's uncomfortably hot!` 46 | ); 47 | } 48 | }; 49 | 50 | // Subscribe to changes 51 | monitor.subscribe(alertTemperature); 52 | 53 | // Set a new temperature 54 | monitor.setTemperature(35); // Logs: 'WARNING: Temperature too high!' 55 | 56 | // Unsubscribe from changes 57 | monitor.unsubscribe(alertTemperature); 58 | 59 | // Set a new temperature 60 | monitor.setTemperature(40); // Does not log anything because we have unsubscribed. 61 | -------------------------------------------------------------------------------- /05_03b/script.js: -------------------------------------------------------------------------------- 1 | // Reactive Object 2 | class ReactiveObject { 3 | constructor() { 4 | this.subscribers = []; 5 | } 6 | 7 | // Subscribe to changes 8 | subscribe(callback) { 9 | // Add callback to subscribers 10 | this.subscribers.push(callback); 11 | } 12 | 13 | // Unsubscribe from changes 14 | unsubscribe(callback) { 15 | // Remove the callback from subscribers 16 | this.subscribers = this.subscribers.filter((sub) => sub !== callback); 17 | } 18 | 19 | updateSubscribers() { 20 | this.subscribers.forEach((callback) => callback(this)); 21 | } 22 | } 23 | 24 | // Temperature Monitor Object 25 | class TemperatureMonitor extends ReactiveObject { 26 | constructor() { 27 | super(); 28 | this.temperature = 0; 29 | } 30 | 31 | // Set temperature 32 | setTemperature(newTemperature) { 33 | this.temperature = newTemperature; 34 | this.updateSubscribers(); 35 | } 36 | } 37 | 38 | // Now we can use this reactive object: 39 | const monitor = new TemperatureMonitor(); 40 | 41 | // Define a callback for temperature changes 42 | const alertTemperature = (monitor) => { 43 | if (monitor.temperature > 30) { 44 | console.log( 45 | `WARNING: It's ${monitor.temperature}°C. It's uncomfortably hot!` 46 | ); 47 | } else { 48 | console.log( 49 | `The temperature is ${monitor.temperature}°C.` 50 | ); 51 | } 52 | }; 53 | 54 | // Subscribe to changes 55 | monitor.subscribe(alertTemperature); 56 | 57 | // Set a new temperature 58 | monitor.setTemperature(35); // Logs: 'WARNING: Temperature too high!' 59 | 60 | // Unsubscribe from changes 61 | monitor.unsubscribe(alertTemperature); 62 | 63 | // Set a new temperature 64 | monitor.setTemperature(40); // Does not log anything because we have unsubscribed. 65 | -------------------------------------------------------------------------------- /05_03e/script.js: -------------------------------------------------------------------------------- 1 | // Reactive Object 2 | class ReactiveObject { 3 | constructor() { 4 | this.subscribers = []; 5 | } 6 | 7 | // Subscribe to changes 8 | subscribe(callback) { 9 | // Add callback to subscribers 10 | this.subscribers.push(callback); 11 | } 12 | 13 | // Unsubscribe from changes 14 | unsubscribe(callback) { 15 | // Remove the callback from subscribers 16 | this.subscribers = this.subscribers.filter((sub) => sub !== callback); 17 | } 18 | 19 | updateSubscribers() { 20 | this.subscribers.forEach((callback) => callback(this)); 21 | } 22 | } 23 | 24 | // Temperature Monitor Object 25 | class TemperatureMonitor extends ReactiveObject { 26 | constructor() { 27 | super(); 28 | this.temperature = 0; 29 | } 30 | 31 | // Set temperature 32 | setTemperature(newTemperature) { 33 | this.temperature = newTemperature; 34 | this.updateSubscribers(); 35 | } 36 | } 37 | 38 | // Cooling System Object 39 | class CoolingSystem extends ReactiveObject { 40 | constructor() { 41 | super(); 42 | this.status = "OFF"; 43 | } 44 | 45 | // Trigger Cooling System 46 | triggerCoolingSystem(monitor) { 47 | if (monitor.temperature > 30) { 48 | this.status = "ON"; 49 | console.log(`Cooling System Activated. Current Status: ${this.status}`); 50 | } else { 51 | this.status = "OFF"; 52 | console.log(`Cooling System Deactivated. Current Status: ${this.status}`); 53 | } 54 | } 55 | } 56 | 57 | // Now we can use these reactive objects together: 58 | let monitor = new TemperatureMonitor(); 59 | let coolingSystem = new CoolingSystem(); 60 | 61 | // Define a callback for temperature changes 62 | let alertTemperature = (monitor) => { 63 | console.log(`Temperature has changed to ${monitor.temperature}°C`); 64 | if (monitor.temperature > 30) { 65 | console.log( 66 | `WARNING: It's ${monitor.temperature}°C. It's uncomfortably hot!` 67 | ); 68 | } 69 | }; 70 | 71 | // Subscribe to changes 72 | monitor.subscribe(alertTemperature); 73 | monitor.subscribe(coolingSystem.triggerCoolingSystem.bind(coolingSystem)); 74 | 75 | // Set a new temperature 76 | monitor.setTemperature(35); 77 | 78 | // Set a new temperature 79 | monitor.setTemperature(28); 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Contribution Agreement 3 | ====================== 4 | 5 | This repository does not accept pull requests (PRs). All pull requests will be closed. 6 | 7 | However, if any contributions (through pull requests, issues, feedback or otherwise) are provided, as a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code (or otherwise providing feedback), you (and, if applicable, your employer) are licensing the submitted code (and/or feedback) to LinkedIn and the open source community subject to the BSD 2-Clause license. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LinkedIn Learning Exercise Files License Agreement 2 | ================================================== 3 | 4 | This License Agreement (the "Agreement") is a binding legal agreement 5 | between you (as an individual or entity, as applicable) and LinkedIn 6 | Corporation (“LinkedIn”). By downloading or using the LinkedIn Learning 7 | exercise files in this repository (“Licensed Materials”), you agree to 8 | be bound by the terms of this Agreement. If you do not agree to these 9 | terms, do not download or use the Licensed Materials. 10 | 11 | 1. License. 12 | - a. Subject to the terms of this Agreement, LinkedIn hereby grants LinkedIn 13 | members during their LinkedIn Learning subscription a non-exclusive, 14 | non-transferable copyright license, for internal use only, to 1) make a 15 | reasonable number of copies of the Licensed Materials, and 2) make 16 | derivative works of the Licensed Materials for the sole purpose of 17 | practicing skills taught in LinkedIn Learning courses. 18 | - b. Distribution. Unless otherwise noted in the Licensed Materials, subject 19 | to the terms of this Agreement, LinkedIn hereby grants LinkedIn members 20 | with a LinkedIn Learning subscription a non-exclusive, non-transferable 21 | copyright license to distribute the Licensed Materials, except the 22 | Licensed Materials may not be included in any product or service (or 23 | otherwise used) to instruct or educate others. 24 | 25 | 2. Restrictions and Intellectual Property. 26 | - a. You may not to use, modify, copy, make derivative works of, publish, 27 | distribute, rent, lease, sell, sublicense, assign or otherwise transfer the 28 | Licensed Materials, except as expressly set forth above in Section 1. 29 | - b. Linkedin (and its licensors) retains its intellectual property rights 30 | in the Licensed Materials. Except as expressly set forth in Section 1, 31 | LinkedIn grants no licenses. 32 | - c. You indemnify LinkedIn and its licensors and affiliates for i) any 33 | alleged infringement or misappropriation of any intellectual property rights 34 | of any third party based on modifications you make to the Licensed Materials, 35 | ii) any claims arising from your use or distribution of all or part of the 36 | Licensed Materials and iii) a breach of this Agreement. You will defend, hold 37 | harmless, and indemnify LinkedIn and its affiliates (and our and their 38 | respective employees, shareholders, and directors) from any claim or action 39 | brought by a third party, including all damages, liabilities, costs and 40 | expenses, including reasonable attorneys’ fees, to the extent resulting from, 41 | alleged to have resulted from, or in connection with: (a) your breach of your 42 | obligations herein; or (b) your use or distribution of any Licensed Materials. 43 | 44 | 3. Open source. This code may include open source software, which may be 45 | subject to other license terms as provided in the files. 46 | 47 | 4. Warranty Disclaimer. LINKEDIN PROVIDES THE LICENSED MATERIALS ON AN “AS IS” 48 | AND “AS AVAILABLE” BASIS. LINKEDIN MAKES NO REPRESENTATION OR WARRANTY, 49 | WHETHER EXPRESS OR IMPLIED, ABOUT THE LICENSED MATERIALS, INCLUDING ANY 50 | REPRESENTATION THAT THE LICENSED MATERIALS WILL BE FREE OF ERRORS, BUGS OR 51 | INTERRUPTIONS, OR THAT THE LICENSED MATERIALS ARE ACCURATE, COMPLETE OR 52 | OTHERWISE VALID. TO THE FULLEST EXTENT PERMITTED BY LAW, LINKEDIN AND ITS 53 | AFFILIATES DISCLAIM ANY IMPLIED OR STATUTORY WARRANTY OR CONDITION, INCLUDING 54 | ANY IMPLIED WARRANTY OR CONDITION OF MERCHANTABILITY OR FITNESS FOR A 55 | PARTICULAR PURPOSE, AVAILABILITY, SECURITY, TITLE AND/OR NON-INFRINGEMENT. 56 | YOUR USE OF THE LICENSED MATERIALS IS AT YOUR OWN DISCRETION AND RISK, AND 57 | YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE THAT RESULTS FROM USE OF THE 58 | LICENSED MATERIALS TO YOUR COMPUTER SYSTEM OR LOSS OF DATA. NO ADVICE OR 59 | INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US OR THROUGH OR 60 | FROM THE LICENSED MATERIALS WILL CREATE ANY WARRANTY OR CONDITION NOT 61 | EXPRESSLY STATED IN THESE TERMS. 62 | 63 | 5. Limitation of Liability. LINKEDIN SHALL NOT BE LIABLE FOR ANY INDIRECT, 64 | INCIDENTAL, SPECIAL, PUNITIVE, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING 65 | BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER 66 | INTANGIBLE LOSSES . IN NO EVENT WILL LINKEDIN'S AGGREGATE LIABILITY TO YOU 67 | EXCEED $100. THIS LIMITATION OF LIABILITY SHALL: 68 | - i. APPLY REGARDLESS OF WHETHER (A) YOU BASE YOUR CLAIM ON CONTRACT, TORT, 69 | STATUTE, OR ANY OTHER LEGAL THEORY, (B) WE KNEW OR SHOULD HAVE KNOWN ABOUT 70 | THE POSSIBILITY OF SUCH DAMAGES, OR (C) THE LIMITED REMEDIES PROVIDED IN THIS 71 | SECTION FAIL OF THEIR ESSENTIAL PURPOSE; AND 72 | - ii. NOT APPLY TO ANY DAMAGE THAT LINKEDIN MAY CAUSE YOU INTENTIONALLY OR 73 | KNOWINGLY IN VIOLATION OF THESE TERMS OR APPLICABLE LAW, OR AS OTHERWISE 74 | MANDATED BY APPLICABLE LAW THAT CANNOT BE DISCLAIMED IN THESE TERMS. 75 | 76 | 6. Termination. This Agreement automatically terminates upon your breach of 77 | this Agreement or termination of your LinkedIn Learning subscription. On 78 | termination, all licenses granted under this Agreement will terminate 79 | immediately and you will delete the Licensed Materials. Sections 2-7 of this 80 | Agreement survive any termination of this Agreement. LinkedIn may discontinue 81 | the availability of some or all of the Licensed Materials at any time for any 82 | reason. 83 | 84 | 7. Miscellaneous. This Agreement will be governed by and construed in 85 | accordance with the laws of the State of California without regard to conflict 86 | of laws principles. The exclusive forum for any disputes arising out of or 87 | relating to this Agreement shall be an appropriate federal or state court 88 | sitting in the County of Santa Clara, State of California. If LinkedIn does 89 | not act to enforce a breach of this Agreement, that does not mean that 90 | LinkedIn has waived its right to enforce this Agreement. The Agreement does 91 | not create a partnership, agency relationship, or joint venture between the 92 | parties. Neither party has the power or authority to bind the other or to 93 | create any obligation or responsibility on behalf of the other. You may not, 94 | without LinkedIn’s prior written consent, assign or delegate any rights or 95 | obligations under these terms, including in connection with a change of 96 | control. Any purported assignment and delegation shall be ineffective. The 97 | Agreement shall bind and inure to the benefit of the parties, their respective 98 | successors and permitted assigns. If any provision of the Agreement is 99 | unenforceable, that provision will be modified to render it enforceable to the 100 | extent possible to give effect to the parties’ intentions and the remaining 101 | provisions will not be affected. This Agreement is the only agreement between 102 | you and LinkedIn regarding the Licensed Materials, and supersedes all prior 103 | agreements relating to the Licensed Materials. 104 | 105 | Last Updated: March 2019 106 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2023 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the LinkedIn Learning Exercise File License (the "License"). 5 | See LICENSE in the project root for license information. 6 | 7 | ATTRIBUTIONS: 8 | 9 | ESLint 10 | https://github.com/eslint/eslint 11 | Copyright OpenJS Foundation and other contributors, 12 | License: MIT 13 | https://opensource.org/licenses/MIT 14 | 15 | Please note, this project may automatically load third party code from external 16 | repositories (for example, NPM modules, Composer packages, or other dependencies). 17 | If so, such third party code may be subject to other license terms than as set 18 | forth above. In addition, such third party code may also depend on and load 19 | multiple tiers of dependencies. Please review the applicable licenses of the 20 | additional dependencies. 21 | 22 | 23 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 24 | 25 | "Copyright OpenJS Foundation and other contributors, 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the ""Software""), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in 35 | all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 43 | THE SOFTWARE." 44 | 45 | =-=-=-=-=-=-=-=-=-=-=-=-==-=-=-=-=-=-=-=-=-=-=-=-=-=- 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript: Five Advanced Challenges and Concepts 2 | This is the repository for the LinkedIn Learning course JavaScript: Five Advanced Challenges and Concepts. The full course is available from [LinkedIn Learning][lil-course-url]. 3 | 4 | ![JavaScript: Five Advanced Challenges and Concepts][lil-thumbnail-url] 5 | 6 | When you’re learning JavaScript, you rarely get time to really dig into the more complex aspects of the language. Until now! This hybrid course mixes learning and hands-on practice with interactive coding challenges. Instructor Morten Rand-Hendriksen guides you through five advanced topics in JavaScript development: turning flat arrays into nested arrays, extending classes, using singletons and proxies, creating observable objects, and building robust applications with the reactive object pattern. Each chapter presents you with a basic introduction, two hands-on examples, and two coding challenges to solidify your learning. Advancing your skills in JavaScript means pushing yourself to go beyond the basics, and Morten shows you how to get it right. 7 | 8 | 9 | 10 | This course includes Code Challenges powered by CoderPad. Code Challenges are interactive coding exercises with real-time feedback, so you can get hands-on coding practice alongside the course content to advance your programming skills. 11 | 12 | ## Instructions 13 | 14 | This repository has folders for each of the lesson videos in the course. The folders are structured to correspond to the videos in the course. The naming convention is `CHAPTER#_MOVIE#`. As an example, the folder named `02_03` corresponds to the second chapter and the third video in that chapter. 15 | 16 | Some folders will have a beginning and an end state. These are marked with the letters `b` for "beginning" and `e` for "end". The `b` folder contains the code as it is at the beginning of the movie. The `e` folder contains the code as it is at the end of the movie. 17 | 18 | ## Coding Challenges 19 | 20 | Each chapter in this course contains two coding challenges. The challenges themselves are powered by CoderPad and appear in the video player as you progress through the course. For each challenge there is a solution video where the instructor walks through the solution. 21 | 22 | ### Coding challenge solutions 23 | 24 | Stand-alone examples of solutions to each challenge are available in this repository. They are found under the `/solutions` folder following the same naming convention as above. As an example, a proposed solution for the first challenge in chapter 2 is named `/02_04_solution/solution.js`. 25 | 26 | ## Installing 27 | 28 | 1. To use these exercise files, you must have the following installed: 29 | - Node.js 30 | 2. Clone this repository into your local machine using the terminal (Mac), CMD (Windows), or a GUI tool like SourceTree. 31 | 3. To run the solutions in the `/solutions` folder, open terminal, navigate to the correct folder, and run `node solution.js`. 32 | 33 | ### Instructor 34 | 35 | Morten Rand-Hendriksen 36 | 37 | Developer and Senior Staff Instructor 38 | 39 | 40 | 41 | Check out my other courses on [LinkedIn Learning](https://www.linkedin.com/learning/instructors/morten-rand-hendriksen). 42 | 43 | [lil-course-url]: https://www.linkedin.com/learning/javascript-five-advanced-challenges-and-concepts?dApp=59033956&leis=LAA 44 | [lil-thumbnail-url]: https://media.licdn.com/dms/image/D560DAQEcH-wHrMMtCg/learning-public-crop_675_1200/0/1692902498364?e=2147483647&v=beta&t=kYWP2z8PmLY27msUIo3s2vnmR3kcoR_kGVTlSjiK0JY 45 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInLearning/advance-your-skills-with-javascript-4452106/cc1c11bb480c4563637e811ad413bd8ce9ec2485/favicon.ico -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advance-your-skills-with-javascript-4452106", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "advance-your-skills-with-javascript-4452106", 9 | "version": "1.0.0", 10 | "license": "SEE LICENSE IN `LICENSE`.", 11 | "dependencies": { 12 | "jsdom": "^22.1.0" 13 | }, 14 | "devDependencies": { 15 | "eslint": "^8.40.0", 16 | "eslint-config-prettier": "^8.8.0", 17 | "prettier": "^2.8.8" 18 | } 19 | }, 20 | "node_modules/@eslint-community/eslint-utils": { 21 | "version": "4.4.0", 22 | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", 23 | "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", 24 | "dev": true, 25 | "dependencies": { 26 | "eslint-visitor-keys": "^3.3.0" 27 | }, 28 | "engines": { 29 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 30 | }, 31 | "peerDependencies": { 32 | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 33 | } 34 | }, 35 | "node_modules/@eslint-community/regexpp": { 36 | "version": "4.5.1", 37 | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", 38 | "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", 39 | "dev": true, 40 | "engines": { 41 | "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 42 | } 43 | }, 44 | "node_modules/@eslint/eslintrc": { 45 | "version": "2.0.3", 46 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", 47 | "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", 48 | "dev": true, 49 | "dependencies": { 50 | "ajv": "^6.12.4", 51 | "debug": "^4.3.2", 52 | "espree": "^9.5.2", 53 | "globals": "^13.19.0", 54 | "ignore": "^5.2.0", 55 | "import-fresh": "^3.2.1", 56 | "js-yaml": "^4.1.0", 57 | "minimatch": "^3.1.2", 58 | "strip-json-comments": "^3.1.1" 59 | }, 60 | "engines": { 61 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 62 | }, 63 | "funding": { 64 | "url": "https://opencollective.com/eslint" 65 | } 66 | }, 67 | "node_modules/@eslint/js": { 68 | "version": "8.41.0", 69 | "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", 70 | "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", 71 | "dev": true, 72 | "engines": { 73 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 74 | } 75 | }, 76 | "node_modules/@humanwhocodes/config-array": { 77 | "version": "0.11.10", 78 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", 79 | "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", 80 | "dev": true, 81 | "dependencies": { 82 | "@humanwhocodes/object-schema": "^1.2.1", 83 | "debug": "^4.1.1", 84 | "minimatch": "^3.0.5" 85 | }, 86 | "engines": { 87 | "node": ">=10.10.0" 88 | } 89 | }, 90 | "node_modules/@humanwhocodes/module-importer": { 91 | "version": "1.0.1", 92 | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 93 | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 94 | "dev": true, 95 | "engines": { 96 | "node": ">=12.22" 97 | }, 98 | "funding": { 99 | "type": "github", 100 | "url": "https://github.com/sponsors/nzakas" 101 | } 102 | }, 103 | "node_modules/@humanwhocodes/object-schema": { 104 | "version": "1.2.1", 105 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", 106 | "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", 107 | "dev": true 108 | }, 109 | "node_modules/@nodelib/fs.scandir": { 110 | "version": "2.1.5", 111 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 112 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 113 | "dev": true, 114 | "dependencies": { 115 | "@nodelib/fs.stat": "2.0.5", 116 | "run-parallel": "^1.1.9" 117 | }, 118 | "engines": { 119 | "node": ">= 8" 120 | } 121 | }, 122 | "node_modules/@nodelib/fs.stat": { 123 | "version": "2.0.5", 124 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 125 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 126 | "dev": true, 127 | "engines": { 128 | "node": ">= 8" 129 | } 130 | }, 131 | "node_modules/@nodelib/fs.walk": { 132 | "version": "1.2.8", 133 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 134 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 135 | "dev": true, 136 | "dependencies": { 137 | "@nodelib/fs.scandir": "2.1.5", 138 | "fastq": "^1.6.0" 139 | }, 140 | "engines": { 141 | "node": ">= 8" 142 | } 143 | }, 144 | "node_modules/@tootallnate/once": { 145 | "version": "2.0.0", 146 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", 147 | "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", 148 | "engines": { 149 | "node": ">= 10" 150 | } 151 | }, 152 | "node_modules/abab": { 153 | "version": "2.0.6", 154 | "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", 155 | "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" 156 | }, 157 | "node_modules/acorn": { 158 | "version": "8.8.2", 159 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", 160 | "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", 161 | "dev": true, 162 | "bin": { 163 | "acorn": "bin/acorn" 164 | }, 165 | "engines": { 166 | "node": ">=0.4.0" 167 | } 168 | }, 169 | "node_modules/acorn-jsx": { 170 | "version": "5.3.2", 171 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 172 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 173 | "dev": true, 174 | "peerDependencies": { 175 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 176 | } 177 | }, 178 | "node_modules/agent-base": { 179 | "version": "6.0.2", 180 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 181 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 182 | "dependencies": { 183 | "debug": "4" 184 | }, 185 | "engines": { 186 | "node": ">= 6.0.0" 187 | } 188 | }, 189 | "node_modules/ajv": { 190 | "version": "6.12.6", 191 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 192 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 193 | "dev": true, 194 | "dependencies": { 195 | "fast-deep-equal": "^3.1.1", 196 | "fast-json-stable-stringify": "^2.0.0", 197 | "json-schema-traverse": "^0.4.1", 198 | "uri-js": "^4.2.2" 199 | }, 200 | "funding": { 201 | "type": "github", 202 | "url": "https://github.com/sponsors/epoberezkin" 203 | } 204 | }, 205 | "node_modules/ansi-regex": { 206 | "version": "5.0.1", 207 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 208 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 209 | "dev": true, 210 | "engines": { 211 | "node": ">=8" 212 | } 213 | }, 214 | "node_modules/ansi-styles": { 215 | "version": "4.3.0", 216 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 217 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 218 | "dev": true, 219 | "dependencies": { 220 | "color-convert": "^2.0.1" 221 | }, 222 | "engines": { 223 | "node": ">=8" 224 | }, 225 | "funding": { 226 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 227 | } 228 | }, 229 | "node_modules/argparse": { 230 | "version": "2.0.1", 231 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 232 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 233 | "dev": true 234 | }, 235 | "node_modules/asynckit": { 236 | "version": "0.4.0", 237 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 238 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 239 | }, 240 | "node_modules/balanced-match": { 241 | "version": "1.0.2", 242 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 243 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 244 | "dev": true 245 | }, 246 | "node_modules/brace-expansion": { 247 | "version": "1.1.11", 248 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 249 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 250 | "dev": true, 251 | "dependencies": { 252 | "balanced-match": "^1.0.0", 253 | "concat-map": "0.0.1" 254 | } 255 | }, 256 | "node_modules/callsites": { 257 | "version": "3.1.0", 258 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 259 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 260 | "dev": true, 261 | "engines": { 262 | "node": ">=6" 263 | } 264 | }, 265 | "node_modules/chalk": { 266 | "version": "4.1.2", 267 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 268 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 269 | "dev": true, 270 | "dependencies": { 271 | "ansi-styles": "^4.1.0", 272 | "supports-color": "^7.1.0" 273 | }, 274 | "engines": { 275 | "node": ">=10" 276 | }, 277 | "funding": { 278 | "url": "https://github.com/chalk/chalk?sponsor=1" 279 | } 280 | }, 281 | "node_modules/color-convert": { 282 | "version": "2.0.1", 283 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 284 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 285 | "dev": true, 286 | "dependencies": { 287 | "color-name": "~1.1.4" 288 | }, 289 | "engines": { 290 | "node": ">=7.0.0" 291 | } 292 | }, 293 | "node_modules/color-name": { 294 | "version": "1.1.4", 295 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 296 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 297 | "dev": true 298 | }, 299 | "node_modules/combined-stream": { 300 | "version": "1.0.8", 301 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 302 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 303 | "dependencies": { 304 | "delayed-stream": "~1.0.0" 305 | }, 306 | "engines": { 307 | "node": ">= 0.8" 308 | } 309 | }, 310 | "node_modules/concat-map": { 311 | "version": "0.0.1", 312 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 313 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 314 | "dev": true 315 | }, 316 | "node_modules/cross-spawn": { 317 | "version": "7.0.3", 318 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 319 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 320 | "dev": true, 321 | "dependencies": { 322 | "path-key": "^3.1.0", 323 | "shebang-command": "^2.0.0", 324 | "which": "^2.0.1" 325 | }, 326 | "engines": { 327 | "node": ">= 8" 328 | } 329 | }, 330 | "node_modules/cssstyle": { 331 | "version": "3.0.0", 332 | "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", 333 | "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", 334 | "dependencies": { 335 | "rrweb-cssom": "^0.6.0" 336 | }, 337 | "engines": { 338 | "node": ">=14" 339 | } 340 | }, 341 | "node_modules/data-urls": { 342 | "version": "4.0.0", 343 | "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", 344 | "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", 345 | "dependencies": { 346 | "abab": "^2.0.6", 347 | "whatwg-mimetype": "^3.0.0", 348 | "whatwg-url": "^12.0.0" 349 | }, 350 | "engines": { 351 | "node": ">=14" 352 | } 353 | }, 354 | "node_modules/debug": { 355 | "version": "4.3.4", 356 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 357 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 358 | "dependencies": { 359 | "ms": "2.1.2" 360 | }, 361 | "engines": { 362 | "node": ">=6.0" 363 | }, 364 | "peerDependenciesMeta": { 365 | "supports-color": { 366 | "optional": true 367 | } 368 | } 369 | }, 370 | "node_modules/decimal.js": { 371 | "version": "10.4.3", 372 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", 373 | "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" 374 | }, 375 | "node_modules/deep-is": { 376 | "version": "0.1.4", 377 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 378 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 379 | "dev": true 380 | }, 381 | "node_modules/delayed-stream": { 382 | "version": "1.0.0", 383 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 384 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 385 | "engines": { 386 | "node": ">=0.4.0" 387 | } 388 | }, 389 | "node_modules/doctrine": { 390 | "version": "3.0.0", 391 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 392 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 393 | "dev": true, 394 | "dependencies": { 395 | "esutils": "^2.0.2" 396 | }, 397 | "engines": { 398 | "node": ">=6.0.0" 399 | } 400 | }, 401 | "node_modules/domexception": { 402 | "version": "4.0.0", 403 | "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", 404 | "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", 405 | "dependencies": { 406 | "webidl-conversions": "^7.0.0" 407 | }, 408 | "engines": { 409 | "node": ">=12" 410 | } 411 | }, 412 | "node_modules/entities": { 413 | "version": "4.5.0", 414 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 415 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 416 | "engines": { 417 | "node": ">=0.12" 418 | }, 419 | "funding": { 420 | "url": "https://github.com/fb55/entities?sponsor=1" 421 | } 422 | }, 423 | "node_modules/escape-string-regexp": { 424 | "version": "4.0.0", 425 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 426 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 427 | "dev": true, 428 | "engines": { 429 | "node": ">=10" 430 | }, 431 | "funding": { 432 | "url": "https://github.com/sponsors/sindresorhus" 433 | } 434 | }, 435 | "node_modules/eslint": { 436 | "version": "8.41.0", 437 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", 438 | "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", 439 | "dev": true, 440 | "dependencies": { 441 | "@eslint-community/eslint-utils": "^4.2.0", 442 | "@eslint-community/regexpp": "^4.4.0", 443 | "@eslint/eslintrc": "^2.0.3", 444 | "@eslint/js": "8.41.0", 445 | "@humanwhocodes/config-array": "^0.11.8", 446 | "@humanwhocodes/module-importer": "^1.0.1", 447 | "@nodelib/fs.walk": "^1.2.8", 448 | "ajv": "^6.10.0", 449 | "chalk": "^4.0.0", 450 | "cross-spawn": "^7.0.2", 451 | "debug": "^4.3.2", 452 | "doctrine": "^3.0.0", 453 | "escape-string-regexp": "^4.0.0", 454 | "eslint-scope": "^7.2.0", 455 | "eslint-visitor-keys": "^3.4.1", 456 | "espree": "^9.5.2", 457 | "esquery": "^1.4.2", 458 | "esutils": "^2.0.2", 459 | "fast-deep-equal": "^3.1.3", 460 | "file-entry-cache": "^6.0.1", 461 | "find-up": "^5.0.0", 462 | "glob-parent": "^6.0.2", 463 | "globals": "^13.19.0", 464 | "graphemer": "^1.4.0", 465 | "ignore": "^5.2.0", 466 | "import-fresh": "^3.0.0", 467 | "imurmurhash": "^0.1.4", 468 | "is-glob": "^4.0.0", 469 | "is-path-inside": "^3.0.3", 470 | "js-yaml": "^4.1.0", 471 | "json-stable-stringify-without-jsonify": "^1.0.1", 472 | "levn": "^0.4.1", 473 | "lodash.merge": "^4.6.2", 474 | "minimatch": "^3.1.2", 475 | "natural-compare": "^1.4.0", 476 | "optionator": "^0.9.1", 477 | "strip-ansi": "^6.0.1", 478 | "strip-json-comments": "^3.1.0", 479 | "text-table": "^0.2.0" 480 | }, 481 | "bin": { 482 | "eslint": "bin/eslint.js" 483 | }, 484 | "engines": { 485 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 486 | }, 487 | "funding": { 488 | "url": "https://opencollective.com/eslint" 489 | } 490 | }, 491 | "node_modules/eslint-config-prettier": { 492 | "version": "8.8.0", 493 | "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", 494 | "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", 495 | "dev": true, 496 | "bin": { 497 | "eslint-config-prettier": "bin/cli.js" 498 | }, 499 | "peerDependencies": { 500 | "eslint": ">=7.0.0" 501 | } 502 | }, 503 | "node_modules/eslint-scope": { 504 | "version": "7.2.0", 505 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", 506 | "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", 507 | "dev": true, 508 | "dependencies": { 509 | "esrecurse": "^4.3.0", 510 | "estraverse": "^5.2.0" 511 | }, 512 | "engines": { 513 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 514 | }, 515 | "funding": { 516 | "url": "https://opencollective.com/eslint" 517 | } 518 | }, 519 | "node_modules/eslint-visitor-keys": { 520 | "version": "3.4.1", 521 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", 522 | "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", 523 | "dev": true, 524 | "engines": { 525 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 526 | }, 527 | "funding": { 528 | "url": "https://opencollective.com/eslint" 529 | } 530 | }, 531 | "node_modules/espree": { 532 | "version": "9.5.2", 533 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", 534 | "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", 535 | "dev": true, 536 | "dependencies": { 537 | "acorn": "^8.8.0", 538 | "acorn-jsx": "^5.3.2", 539 | "eslint-visitor-keys": "^3.4.1" 540 | }, 541 | "engines": { 542 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 543 | }, 544 | "funding": { 545 | "url": "https://opencollective.com/eslint" 546 | } 547 | }, 548 | "node_modules/esquery": { 549 | "version": "1.5.0", 550 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", 551 | "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", 552 | "dev": true, 553 | "dependencies": { 554 | "estraverse": "^5.1.0" 555 | }, 556 | "engines": { 557 | "node": ">=0.10" 558 | } 559 | }, 560 | "node_modules/esrecurse": { 561 | "version": "4.3.0", 562 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 563 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 564 | "dev": true, 565 | "dependencies": { 566 | "estraverse": "^5.2.0" 567 | }, 568 | "engines": { 569 | "node": ">=4.0" 570 | } 571 | }, 572 | "node_modules/estraverse": { 573 | "version": "5.3.0", 574 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 575 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 576 | "dev": true, 577 | "engines": { 578 | "node": ">=4.0" 579 | } 580 | }, 581 | "node_modules/esutils": { 582 | "version": "2.0.3", 583 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 584 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 585 | "dev": true, 586 | "engines": { 587 | "node": ">=0.10.0" 588 | } 589 | }, 590 | "node_modules/fast-deep-equal": { 591 | "version": "3.1.3", 592 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 593 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 594 | "dev": true 595 | }, 596 | "node_modules/fast-json-stable-stringify": { 597 | "version": "2.1.0", 598 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 599 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 600 | "dev": true 601 | }, 602 | "node_modules/fast-levenshtein": { 603 | "version": "2.0.6", 604 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 605 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 606 | "dev": true 607 | }, 608 | "node_modules/fastq": { 609 | "version": "1.15.0", 610 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", 611 | "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", 612 | "dev": true, 613 | "dependencies": { 614 | "reusify": "^1.0.4" 615 | } 616 | }, 617 | "node_modules/file-entry-cache": { 618 | "version": "6.0.1", 619 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 620 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 621 | "dev": true, 622 | "dependencies": { 623 | "flat-cache": "^3.0.4" 624 | }, 625 | "engines": { 626 | "node": "^10.12.0 || >=12.0.0" 627 | } 628 | }, 629 | "node_modules/find-up": { 630 | "version": "5.0.0", 631 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 632 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 633 | "dev": true, 634 | "dependencies": { 635 | "locate-path": "^6.0.0", 636 | "path-exists": "^4.0.0" 637 | }, 638 | "engines": { 639 | "node": ">=10" 640 | }, 641 | "funding": { 642 | "url": "https://github.com/sponsors/sindresorhus" 643 | } 644 | }, 645 | "node_modules/flat-cache": { 646 | "version": "3.0.4", 647 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", 648 | "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", 649 | "dev": true, 650 | "dependencies": { 651 | "flatted": "^3.1.0", 652 | "rimraf": "^3.0.2" 653 | }, 654 | "engines": { 655 | "node": "^10.12.0 || >=12.0.0" 656 | } 657 | }, 658 | "node_modules/flatted": { 659 | "version": "3.2.7", 660 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", 661 | "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", 662 | "dev": true 663 | }, 664 | "node_modules/form-data": { 665 | "version": "4.0.0", 666 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 667 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 668 | "dependencies": { 669 | "asynckit": "^0.4.0", 670 | "combined-stream": "^1.0.8", 671 | "mime-types": "^2.1.12" 672 | }, 673 | "engines": { 674 | "node": ">= 6" 675 | } 676 | }, 677 | "node_modules/fs.realpath": { 678 | "version": "1.0.0", 679 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 680 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 681 | "dev": true 682 | }, 683 | "node_modules/glob": { 684 | "version": "7.2.3", 685 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 686 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 687 | "dev": true, 688 | "dependencies": { 689 | "fs.realpath": "^1.0.0", 690 | "inflight": "^1.0.4", 691 | "inherits": "2", 692 | "minimatch": "^3.1.1", 693 | "once": "^1.3.0", 694 | "path-is-absolute": "^1.0.0" 695 | }, 696 | "engines": { 697 | "node": "*" 698 | }, 699 | "funding": { 700 | "url": "https://github.com/sponsors/isaacs" 701 | } 702 | }, 703 | "node_modules/glob-parent": { 704 | "version": "6.0.2", 705 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 706 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 707 | "dev": true, 708 | "dependencies": { 709 | "is-glob": "^4.0.3" 710 | }, 711 | "engines": { 712 | "node": ">=10.13.0" 713 | } 714 | }, 715 | "node_modules/globals": { 716 | "version": "13.20.0", 717 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", 718 | "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", 719 | "dev": true, 720 | "dependencies": { 721 | "type-fest": "^0.20.2" 722 | }, 723 | "engines": { 724 | "node": ">=8" 725 | }, 726 | "funding": { 727 | "url": "https://github.com/sponsors/sindresorhus" 728 | } 729 | }, 730 | "node_modules/graphemer": { 731 | "version": "1.4.0", 732 | "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 733 | "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 734 | "dev": true 735 | }, 736 | "node_modules/has-flag": { 737 | "version": "4.0.0", 738 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 739 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 740 | "dev": true, 741 | "engines": { 742 | "node": ">=8" 743 | } 744 | }, 745 | "node_modules/html-encoding-sniffer": { 746 | "version": "3.0.0", 747 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", 748 | "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", 749 | "dependencies": { 750 | "whatwg-encoding": "^2.0.0" 751 | }, 752 | "engines": { 753 | "node": ">=12" 754 | } 755 | }, 756 | "node_modules/http-proxy-agent": { 757 | "version": "5.0.0", 758 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", 759 | "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", 760 | "dependencies": { 761 | "@tootallnate/once": "2", 762 | "agent-base": "6", 763 | "debug": "4" 764 | }, 765 | "engines": { 766 | "node": ">= 6" 767 | } 768 | }, 769 | "node_modules/https-proxy-agent": { 770 | "version": "5.0.1", 771 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 772 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 773 | "dependencies": { 774 | "agent-base": "6", 775 | "debug": "4" 776 | }, 777 | "engines": { 778 | "node": ">= 6" 779 | } 780 | }, 781 | "node_modules/iconv-lite": { 782 | "version": "0.6.3", 783 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 784 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 785 | "dependencies": { 786 | "safer-buffer": ">= 2.1.2 < 3.0.0" 787 | }, 788 | "engines": { 789 | "node": ">=0.10.0" 790 | } 791 | }, 792 | "node_modules/ignore": { 793 | "version": "5.2.4", 794 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", 795 | "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", 796 | "dev": true, 797 | "engines": { 798 | "node": ">= 4" 799 | } 800 | }, 801 | "node_modules/import-fresh": { 802 | "version": "3.3.0", 803 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 804 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 805 | "dev": true, 806 | "dependencies": { 807 | "parent-module": "^1.0.0", 808 | "resolve-from": "^4.0.0" 809 | }, 810 | "engines": { 811 | "node": ">=6" 812 | }, 813 | "funding": { 814 | "url": "https://github.com/sponsors/sindresorhus" 815 | } 816 | }, 817 | "node_modules/imurmurhash": { 818 | "version": "0.1.4", 819 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 820 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 821 | "dev": true, 822 | "engines": { 823 | "node": ">=0.8.19" 824 | } 825 | }, 826 | "node_modules/inflight": { 827 | "version": "1.0.6", 828 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 829 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 830 | "dev": true, 831 | "dependencies": { 832 | "once": "^1.3.0", 833 | "wrappy": "1" 834 | } 835 | }, 836 | "node_modules/inherits": { 837 | "version": "2.0.4", 838 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 839 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 840 | "dev": true 841 | }, 842 | "node_modules/is-extglob": { 843 | "version": "2.1.1", 844 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 845 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 846 | "dev": true, 847 | "engines": { 848 | "node": ">=0.10.0" 849 | } 850 | }, 851 | "node_modules/is-glob": { 852 | "version": "4.0.3", 853 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 854 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 855 | "dev": true, 856 | "dependencies": { 857 | "is-extglob": "^2.1.1" 858 | }, 859 | "engines": { 860 | "node": ">=0.10.0" 861 | } 862 | }, 863 | "node_modules/is-path-inside": { 864 | "version": "3.0.3", 865 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", 866 | "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", 867 | "dev": true, 868 | "engines": { 869 | "node": ">=8" 870 | } 871 | }, 872 | "node_modules/is-potential-custom-element-name": { 873 | "version": "1.0.1", 874 | "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", 875 | "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" 876 | }, 877 | "node_modules/isexe": { 878 | "version": "2.0.0", 879 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 880 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 881 | "dev": true 882 | }, 883 | "node_modules/js-yaml": { 884 | "version": "4.1.0", 885 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 886 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 887 | "dev": true, 888 | "dependencies": { 889 | "argparse": "^2.0.1" 890 | }, 891 | "bin": { 892 | "js-yaml": "bin/js-yaml.js" 893 | } 894 | }, 895 | "node_modules/jsdom": { 896 | "version": "22.1.0", 897 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", 898 | "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", 899 | "dependencies": { 900 | "abab": "^2.0.6", 901 | "cssstyle": "^3.0.0", 902 | "data-urls": "^4.0.0", 903 | "decimal.js": "^10.4.3", 904 | "domexception": "^4.0.0", 905 | "form-data": "^4.0.0", 906 | "html-encoding-sniffer": "^3.0.0", 907 | "http-proxy-agent": "^5.0.0", 908 | "https-proxy-agent": "^5.0.1", 909 | "is-potential-custom-element-name": "^1.0.1", 910 | "nwsapi": "^2.2.4", 911 | "parse5": "^7.1.2", 912 | "rrweb-cssom": "^0.6.0", 913 | "saxes": "^6.0.0", 914 | "symbol-tree": "^3.2.4", 915 | "tough-cookie": "^4.1.2", 916 | "w3c-xmlserializer": "^4.0.0", 917 | "webidl-conversions": "^7.0.0", 918 | "whatwg-encoding": "^2.0.0", 919 | "whatwg-mimetype": "^3.0.0", 920 | "whatwg-url": "^12.0.1", 921 | "ws": "^8.13.0", 922 | "xml-name-validator": "^4.0.0" 923 | }, 924 | "engines": { 925 | "node": ">=16" 926 | }, 927 | "peerDependencies": { 928 | "canvas": "^2.5.0" 929 | }, 930 | "peerDependenciesMeta": { 931 | "canvas": { 932 | "optional": true 933 | } 934 | } 935 | }, 936 | "node_modules/json-schema-traverse": { 937 | "version": "0.4.1", 938 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 939 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 940 | "dev": true 941 | }, 942 | "node_modules/json-stable-stringify-without-jsonify": { 943 | "version": "1.0.1", 944 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 945 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 946 | "dev": true 947 | }, 948 | "node_modules/levn": { 949 | "version": "0.4.1", 950 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 951 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 952 | "dev": true, 953 | "dependencies": { 954 | "prelude-ls": "^1.2.1", 955 | "type-check": "~0.4.0" 956 | }, 957 | "engines": { 958 | "node": ">= 0.8.0" 959 | } 960 | }, 961 | "node_modules/locate-path": { 962 | "version": "6.0.0", 963 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 964 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 965 | "dev": true, 966 | "dependencies": { 967 | "p-locate": "^5.0.0" 968 | }, 969 | "engines": { 970 | "node": ">=10" 971 | }, 972 | "funding": { 973 | "url": "https://github.com/sponsors/sindresorhus" 974 | } 975 | }, 976 | "node_modules/lodash.merge": { 977 | "version": "4.6.2", 978 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 979 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 980 | "dev": true 981 | }, 982 | "node_modules/mime-db": { 983 | "version": "1.52.0", 984 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 985 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 986 | "engines": { 987 | "node": ">= 0.6" 988 | } 989 | }, 990 | "node_modules/mime-types": { 991 | "version": "2.1.35", 992 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 993 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 994 | "dependencies": { 995 | "mime-db": "1.52.0" 996 | }, 997 | "engines": { 998 | "node": ">= 0.6" 999 | } 1000 | }, 1001 | "node_modules/minimatch": { 1002 | "version": "3.1.2", 1003 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1004 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1005 | "dev": true, 1006 | "dependencies": { 1007 | "brace-expansion": "^1.1.7" 1008 | }, 1009 | "engines": { 1010 | "node": "*" 1011 | } 1012 | }, 1013 | "node_modules/ms": { 1014 | "version": "2.1.2", 1015 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1016 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1017 | }, 1018 | "node_modules/natural-compare": { 1019 | "version": "1.4.0", 1020 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1021 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 1022 | "dev": true 1023 | }, 1024 | "node_modules/nwsapi": { 1025 | "version": "2.2.5", 1026 | "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.5.tgz", 1027 | "integrity": "sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==" 1028 | }, 1029 | "node_modules/once": { 1030 | "version": "1.4.0", 1031 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1032 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1033 | "dev": true, 1034 | "dependencies": { 1035 | "wrappy": "1" 1036 | } 1037 | }, 1038 | "node_modules/optionator": { 1039 | "version": "0.9.1", 1040 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 1041 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 1042 | "dev": true, 1043 | "dependencies": { 1044 | "deep-is": "^0.1.3", 1045 | "fast-levenshtein": "^2.0.6", 1046 | "levn": "^0.4.1", 1047 | "prelude-ls": "^1.2.1", 1048 | "type-check": "^0.4.0", 1049 | "word-wrap": "^1.2.3" 1050 | }, 1051 | "engines": { 1052 | "node": ">= 0.8.0" 1053 | } 1054 | }, 1055 | "node_modules/p-limit": { 1056 | "version": "3.1.0", 1057 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1058 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1059 | "dev": true, 1060 | "dependencies": { 1061 | "yocto-queue": "^0.1.0" 1062 | }, 1063 | "engines": { 1064 | "node": ">=10" 1065 | }, 1066 | "funding": { 1067 | "url": "https://github.com/sponsors/sindresorhus" 1068 | } 1069 | }, 1070 | "node_modules/p-locate": { 1071 | "version": "5.0.0", 1072 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1073 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1074 | "dev": true, 1075 | "dependencies": { 1076 | "p-limit": "^3.0.2" 1077 | }, 1078 | "engines": { 1079 | "node": ">=10" 1080 | }, 1081 | "funding": { 1082 | "url": "https://github.com/sponsors/sindresorhus" 1083 | } 1084 | }, 1085 | "node_modules/parent-module": { 1086 | "version": "1.0.1", 1087 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1088 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1089 | "dev": true, 1090 | "dependencies": { 1091 | "callsites": "^3.0.0" 1092 | }, 1093 | "engines": { 1094 | "node": ">=6" 1095 | } 1096 | }, 1097 | "node_modules/parse5": { 1098 | "version": "7.1.2", 1099 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", 1100 | "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", 1101 | "dependencies": { 1102 | "entities": "^4.4.0" 1103 | }, 1104 | "funding": { 1105 | "url": "https://github.com/inikulin/parse5?sponsor=1" 1106 | } 1107 | }, 1108 | "node_modules/path-exists": { 1109 | "version": "4.0.0", 1110 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1111 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1112 | "dev": true, 1113 | "engines": { 1114 | "node": ">=8" 1115 | } 1116 | }, 1117 | "node_modules/path-is-absolute": { 1118 | "version": "1.0.1", 1119 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1120 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 1121 | "dev": true, 1122 | "engines": { 1123 | "node": ">=0.10.0" 1124 | } 1125 | }, 1126 | "node_modules/path-key": { 1127 | "version": "3.1.1", 1128 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1129 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1130 | "dev": true, 1131 | "engines": { 1132 | "node": ">=8" 1133 | } 1134 | }, 1135 | "node_modules/prelude-ls": { 1136 | "version": "1.2.1", 1137 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 1138 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 1139 | "dev": true, 1140 | "engines": { 1141 | "node": ">= 0.8.0" 1142 | } 1143 | }, 1144 | "node_modules/prettier": { 1145 | "version": "2.8.8", 1146 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", 1147 | "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", 1148 | "dev": true, 1149 | "bin": { 1150 | "prettier": "bin-prettier.js" 1151 | }, 1152 | "engines": { 1153 | "node": ">=10.13.0" 1154 | }, 1155 | "funding": { 1156 | "url": "https://github.com/prettier/prettier?sponsor=1" 1157 | } 1158 | }, 1159 | "node_modules/psl": { 1160 | "version": "1.9.0", 1161 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", 1162 | "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" 1163 | }, 1164 | "node_modules/punycode": { 1165 | "version": "2.3.0", 1166 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", 1167 | "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", 1168 | "engines": { 1169 | "node": ">=6" 1170 | } 1171 | }, 1172 | "node_modules/querystringify": { 1173 | "version": "2.2.0", 1174 | "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", 1175 | "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" 1176 | }, 1177 | "node_modules/queue-microtask": { 1178 | "version": "1.2.3", 1179 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1180 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1181 | "dev": true, 1182 | "funding": [ 1183 | { 1184 | "type": "github", 1185 | "url": "https://github.com/sponsors/feross" 1186 | }, 1187 | { 1188 | "type": "patreon", 1189 | "url": "https://www.patreon.com/feross" 1190 | }, 1191 | { 1192 | "type": "consulting", 1193 | "url": "https://feross.org/support" 1194 | } 1195 | ] 1196 | }, 1197 | "node_modules/requires-port": { 1198 | "version": "1.0.0", 1199 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 1200 | "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" 1201 | }, 1202 | "node_modules/resolve-from": { 1203 | "version": "4.0.0", 1204 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1205 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1206 | "dev": true, 1207 | "engines": { 1208 | "node": ">=4" 1209 | } 1210 | }, 1211 | "node_modules/reusify": { 1212 | "version": "1.0.4", 1213 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1214 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1215 | "dev": true, 1216 | "engines": { 1217 | "iojs": ">=1.0.0", 1218 | "node": ">=0.10.0" 1219 | } 1220 | }, 1221 | "node_modules/rimraf": { 1222 | "version": "3.0.2", 1223 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1224 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1225 | "dev": true, 1226 | "dependencies": { 1227 | "glob": "^7.1.3" 1228 | }, 1229 | "bin": { 1230 | "rimraf": "bin.js" 1231 | }, 1232 | "funding": { 1233 | "url": "https://github.com/sponsors/isaacs" 1234 | } 1235 | }, 1236 | "node_modules/rrweb-cssom": { 1237 | "version": "0.6.0", 1238 | "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", 1239 | "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" 1240 | }, 1241 | "node_modules/run-parallel": { 1242 | "version": "1.2.0", 1243 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1244 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1245 | "dev": true, 1246 | "funding": [ 1247 | { 1248 | "type": "github", 1249 | "url": "https://github.com/sponsors/feross" 1250 | }, 1251 | { 1252 | "type": "patreon", 1253 | "url": "https://www.patreon.com/feross" 1254 | }, 1255 | { 1256 | "type": "consulting", 1257 | "url": "https://feross.org/support" 1258 | } 1259 | ], 1260 | "dependencies": { 1261 | "queue-microtask": "^1.2.2" 1262 | } 1263 | }, 1264 | "node_modules/safer-buffer": { 1265 | "version": "2.1.2", 1266 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1267 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1268 | }, 1269 | "node_modules/saxes": { 1270 | "version": "6.0.0", 1271 | "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", 1272 | "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", 1273 | "dependencies": { 1274 | "xmlchars": "^2.2.0" 1275 | }, 1276 | "engines": { 1277 | "node": ">=v12.22.7" 1278 | } 1279 | }, 1280 | "node_modules/shebang-command": { 1281 | "version": "2.0.0", 1282 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1283 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1284 | "dev": true, 1285 | "dependencies": { 1286 | "shebang-regex": "^3.0.0" 1287 | }, 1288 | "engines": { 1289 | "node": ">=8" 1290 | } 1291 | }, 1292 | "node_modules/shebang-regex": { 1293 | "version": "3.0.0", 1294 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1295 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1296 | "dev": true, 1297 | "engines": { 1298 | "node": ">=8" 1299 | } 1300 | }, 1301 | "node_modules/strip-ansi": { 1302 | "version": "6.0.1", 1303 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1304 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1305 | "dev": true, 1306 | "dependencies": { 1307 | "ansi-regex": "^5.0.1" 1308 | }, 1309 | "engines": { 1310 | "node": ">=8" 1311 | } 1312 | }, 1313 | "node_modules/strip-json-comments": { 1314 | "version": "3.1.1", 1315 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1316 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1317 | "dev": true, 1318 | "engines": { 1319 | "node": ">=8" 1320 | }, 1321 | "funding": { 1322 | "url": "https://github.com/sponsors/sindresorhus" 1323 | } 1324 | }, 1325 | "node_modules/supports-color": { 1326 | "version": "7.2.0", 1327 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1328 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1329 | "dev": true, 1330 | "dependencies": { 1331 | "has-flag": "^4.0.0" 1332 | }, 1333 | "engines": { 1334 | "node": ">=8" 1335 | } 1336 | }, 1337 | "node_modules/symbol-tree": { 1338 | "version": "3.2.4", 1339 | "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", 1340 | "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" 1341 | }, 1342 | "node_modules/text-table": { 1343 | "version": "0.2.0", 1344 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1345 | "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", 1346 | "dev": true 1347 | }, 1348 | "node_modules/tough-cookie": { 1349 | "version": "4.1.2", 1350 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", 1351 | "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", 1352 | "dependencies": { 1353 | "psl": "^1.1.33", 1354 | "punycode": "^2.1.1", 1355 | "universalify": "^0.2.0", 1356 | "url-parse": "^1.5.3" 1357 | }, 1358 | "engines": { 1359 | "node": ">=6" 1360 | } 1361 | }, 1362 | "node_modules/tr46": { 1363 | "version": "4.1.1", 1364 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", 1365 | "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", 1366 | "dependencies": { 1367 | "punycode": "^2.3.0" 1368 | }, 1369 | "engines": { 1370 | "node": ">=14" 1371 | } 1372 | }, 1373 | "node_modules/type-check": { 1374 | "version": "0.4.0", 1375 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1376 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1377 | "dev": true, 1378 | "dependencies": { 1379 | "prelude-ls": "^1.2.1" 1380 | }, 1381 | "engines": { 1382 | "node": ">= 0.8.0" 1383 | } 1384 | }, 1385 | "node_modules/type-fest": { 1386 | "version": "0.20.2", 1387 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 1388 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 1389 | "dev": true, 1390 | "engines": { 1391 | "node": ">=10" 1392 | }, 1393 | "funding": { 1394 | "url": "https://github.com/sponsors/sindresorhus" 1395 | } 1396 | }, 1397 | "node_modules/universalify": { 1398 | "version": "0.2.0", 1399 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", 1400 | "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", 1401 | "engines": { 1402 | "node": ">= 4.0.0" 1403 | } 1404 | }, 1405 | "node_modules/uri-js": { 1406 | "version": "4.4.1", 1407 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1408 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1409 | "dev": true, 1410 | "dependencies": { 1411 | "punycode": "^2.1.0" 1412 | } 1413 | }, 1414 | "node_modules/url-parse": { 1415 | "version": "1.5.10", 1416 | "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", 1417 | "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", 1418 | "dependencies": { 1419 | "querystringify": "^2.1.1", 1420 | "requires-port": "^1.0.0" 1421 | } 1422 | }, 1423 | "node_modules/w3c-xmlserializer": { 1424 | "version": "4.0.0", 1425 | "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", 1426 | "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", 1427 | "dependencies": { 1428 | "xml-name-validator": "^4.0.0" 1429 | }, 1430 | "engines": { 1431 | "node": ">=14" 1432 | } 1433 | }, 1434 | "node_modules/webidl-conversions": { 1435 | "version": "7.0.0", 1436 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 1437 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 1438 | "engines": { 1439 | "node": ">=12" 1440 | } 1441 | }, 1442 | "node_modules/whatwg-encoding": { 1443 | "version": "2.0.0", 1444 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", 1445 | "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", 1446 | "dependencies": { 1447 | "iconv-lite": "0.6.3" 1448 | }, 1449 | "engines": { 1450 | "node": ">=12" 1451 | } 1452 | }, 1453 | "node_modules/whatwg-mimetype": { 1454 | "version": "3.0.0", 1455 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", 1456 | "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", 1457 | "engines": { 1458 | "node": ">=12" 1459 | } 1460 | }, 1461 | "node_modules/whatwg-url": { 1462 | "version": "12.0.1", 1463 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", 1464 | "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", 1465 | "dependencies": { 1466 | "tr46": "^4.1.1", 1467 | "webidl-conversions": "^7.0.0" 1468 | }, 1469 | "engines": { 1470 | "node": ">=14" 1471 | } 1472 | }, 1473 | "node_modules/which": { 1474 | "version": "2.0.2", 1475 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1476 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1477 | "dev": true, 1478 | "dependencies": { 1479 | "isexe": "^2.0.0" 1480 | }, 1481 | "bin": { 1482 | "node-which": "bin/node-which" 1483 | }, 1484 | "engines": { 1485 | "node": ">= 8" 1486 | } 1487 | }, 1488 | "node_modules/word-wrap": { 1489 | "version": "1.2.3", 1490 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 1491 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 1492 | "dev": true, 1493 | "engines": { 1494 | "node": ">=0.10.0" 1495 | } 1496 | }, 1497 | "node_modules/wrappy": { 1498 | "version": "1.0.2", 1499 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1500 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1501 | "dev": true 1502 | }, 1503 | "node_modules/ws": { 1504 | "version": "8.13.0", 1505 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", 1506 | "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", 1507 | "engines": { 1508 | "node": ">=10.0.0" 1509 | }, 1510 | "peerDependencies": { 1511 | "bufferutil": "^4.0.1", 1512 | "utf-8-validate": ">=5.0.2" 1513 | }, 1514 | "peerDependenciesMeta": { 1515 | "bufferutil": { 1516 | "optional": true 1517 | }, 1518 | "utf-8-validate": { 1519 | "optional": true 1520 | } 1521 | } 1522 | }, 1523 | "node_modules/xml-name-validator": { 1524 | "version": "4.0.0", 1525 | "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", 1526 | "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", 1527 | "engines": { 1528 | "node": ">=12" 1529 | } 1530 | }, 1531 | "node_modules/xmlchars": { 1532 | "version": "2.2.0", 1533 | "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", 1534 | "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" 1535 | }, 1536 | "node_modules/yocto-queue": { 1537 | "version": "0.1.0", 1538 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1539 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1540 | "dev": true, 1541 | "engines": { 1542 | "node": ">=10" 1543 | }, 1544 | "funding": { 1545 | "url": "https://github.com/sponsors/sindresorhus" 1546 | } 1547 | } 1548 | } 1549 | } 1550 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advance-your-skills-with-javascript-4452106", 3 | "version": "1.0.0", 4 | "description": "Exercise files for the LinkedIn Learning course \"Advance Your Skills with JavaScript\".", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/LinkedInLearning/advance-your-skills-with-javascript-4452106.git" 11 | }, 12 | "author": "@mor10", 13 | "license": "SEE LICENSE IN `LICENSE`.", 14 | "bugs": { 15 | "url": "https://github.com/LinkedInLearning/advance-your-skills-with-javascript-4452106/issues" 16 | }, 17 | "homepage": "https://github.com/LinkedInLearning/advance-your-skills-with-javascript-4452106#readme", 18 | "dependencies": { 19 | "jsdom": "^22.1.0" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^8.40.0", 23 | "eslint-config-prettier": "^8.8.0", 24 | "prettier": "^2.8.8" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /solutions/01_04_solution/solution.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { id: 1, parent: 0 }, 3 | { id: 2, parent: 0 }, 4 | { id: 3, parent: 1 }, 5 | { id: 4, parent: 1 }, 6 | { id: 5, parent: 1 }, 7 | { id: 6, parent: 2 }, 8 | { id: 7, parent: 2 }, 9 | { id: 8, parent: 2 }, 10 | { id: 9, parent: 3 }, 11 | { id: 10, parent: 3 }, 12 | { id: 11, parent: 3 }, 13 | { id: 12, parent: 3 }, 14 | { id: 13, parent: 3 }, 15 | { id: 14, parent: 3 }, 16 | { id: 15, parent: 3 }, 17 | ]; 18 | 19 | // Restructure the items array into a nested object 20 | function restructureArray(data) { 21 | const dataMap = {}; 22 | const root = []; 23 | 24 | data.forEach((item) => { 25 | dataMap[item.id] = { 26 | ...item, 27 | children: [], 28 | }; 29 | }); 30 | 31 | data.forEach((item) => { 32 | const parent = dataMap[item.parent]; 33 | if (parent) { 34 | parent.children.push(dataMap[item.id]); 35 | } else { 36 | root.push(dataMap[item.id]); 37 | } 38 | }); 39 | 40 | return root; 41 | } 42 | 43 | const result = restructureArray(data); 44 | 45 | // output the resut array as a tree 46 | console.log(JSON.stringify(result, null, 2)); 47 | -------------------------------------------------------------------------------- /solutions/01_06_solution/solution.js: -------------------------------------------------------------------------------- 1 | const jsdom = require("jsdom"); 2 | const { JSDOM } = jsdom; 3 | 4 | /** 5 | * Helper function to output properly indented HTML in the console. 6 | */ 7 | const generateHtmlString = (element, indent = 0) => { 8 | let html = ""; 9 | const indentation = " ".repeat(indent * 2); 10 | 11 | // Opening tag 12 | html += `${indentation}<${element.tagName.toLowerCase()}`; 13 | 14 | // Attributes 15 | Array.from(element.attributes).forEach((attr) => { 16 | html += ` ${attr.name}="${attr.value}"`; 17 | }); 18 | 19 | html += ">"; 20 | 21 | // Text content 22 | if (element.childNodes.length === 1 && element.childNodes[0].nodeType === 3) { 23 | // Only text content 24 | html += element.childNodes[0].textContent.trim(); 25 | } else { 26 | // Children 27 | const children = Array.from(element.children); 28 | if (children.length > 0) { 29 | html += "\n"; 30 | 31 | children.forEach((child) => { 32 | html += generateHtmlString(child, indent + 1); 33 | }); 34 | 35 | html += `${indentation}`; 36 | } 37 | } 38 | 39 | // Closing tag 40 | html += `\n`; 41 | 42 | return html; 43 | }; 44 | 45 | /** 46 | * Scaffolding to provide a node-based DOM to work with. 47 | * @returns 48 | */ 49 | // Create promise-based ad-hoc DOM. 50 | function setupJSDOM() { 51 | return new Promise((resolve) => { 52 | const dom = new JSDOM( 53 | `` 54 | ); 55 | const document = dom.window.document; 56 | resolve(document); 57 | }); 58 | } 59 | 60 | // BEGIN async function to make `document` available in candidate answer code. 61 | const runScript = () => 62 | setupJSDOM() 63 | .then((document) => { 64 | const rootComments = restructureComments(comments); 65 | const result = generateNestedList(rootComments, document); 66 | console.log(generateHtmlString(result)); 67 | }) 68 | 69 | .catch((error) => { 70 | console.error(error); 71 | }); 72 | 73 | // Run the async function. 74 | runScript(); 75 | 76 | /** 77 | * The actual coding challenge solution: 78 | */ 79 | const comments = [ 80 | { 81 | comment_ID: 1, 82 | comment_text: "Top-level comment 1", 83 | comment_parent: 0, 84 | }, 85 | { 86 | comment_ID: 2, 87 | comment_text: "Top-level comment 2", 88 | comment_parent: 0, 89 | }, 90 | { 91 | comment_ID: 3, 92 | comment_text: "Reply to Top-level comment 1, Child comment 1", 93 | comment_parent: 1, 94 | }, 95 | { 96 | comment_ID: 4, 97 | comment_text: "Reply to Top-level comment 1, Child comment 2", 98 | comment_parent: 1, 99 | }, 100 | { 101 | comment_ID: 5, 102 | comment_text: "Reply to Top-level comment 1, Child comment 3", 103 | comment_parent: 1, 104 | }, 105 | { 106 | comment_ID: 6, 107 | comment_text: "Reply to Top-level comment 2, Child comment 1", 108 | comment_parent: 2, 109 | }, 110 | { 111 | comment_ID: 7, 112 | comment_text: "Reply to Top-level comment 2, Child comment 2", 113 | comment_parent: 2, 114 | }, 115 | { 116 | comment_ID: 8, 117 | comment_text: "Reply to Top-level comment 2, Child comment 3", 118 | comment_parent: 2, 119 | }, 120 | { 121 | comment_ID: 9, 122 | comment_text: "Reply to Reply to Top-level comment 1, Grandchild comment 1", 123 | comment_parent: 3, 124 | }, 125 | { 126 | comment_ID: 10, 127 | comment_text: "Reply to Reply to Top-level comment 1, Grandchild comment 2", 128 | comment_parent: 3, 129 | }, 130 | { 131 | comment_ID: 11, 132 | comment_text: "Reply to Reply to Top-level comment 1, Grandchild comment 3", 133 | comment_parent: 3, 134 | }, 135 | { 136 | comment_ID: 12, 137 | comment_text: "Reply to Reply to Top-level comment 1, Grandchild comment 4", 138 | comment_parent: 3, 139 | }, 140 | { 141 | comment_ID: 13, 142 | comment_text: "Reply to Reply to Top-level comment 1, Grandchild comment 5", 143 | comment_parent: 3, 144 | }, 145 | { 146 | comment_ID: 14, 147 | comment_text: "Reply to Reply to Top-level comment 1, Grandchild comment 6", 148 | comment_parent: 3, 149 | }, 150 | { 151 | comment_ID: 15, 152 | comment_text: "Reply to Reply to Top-level comment 1, Grandchild comment 7", 153 | comment_parent: 3, 154 | }, 155 | ]; 156 | 157 | // Restructure the comments array into a nested object 158 | function restructureComments(comments) { 159 | // comments.sort((a, b) => a.comment_ID - b.comment_ID); 160 | const commentsMap = {}; 161 | const rootComments = []; 162 | 163 | // Create a map of comments by their ID 164 | comments.forEach((comment) => { 165 | commentsMap[comment.comment_ID] = { 166 | ...comment, 167 | children: [], 168 | }; 169 | }); 170 | 171 | // Iterate through the comments again and add each one as a child 172 | // of its parent comment, if it has one. If it doesn't have a parent, 173 | // it's a root-level comment and should be added to the `rootComments` array. 174 | comments.forEach((comment) => { 175 | if (comment.comment_parent !== 0) { 176 | const parent = commentsMap[comment.comment_parent]; 177 | if (parent) { 178 | parent.children.push(commentsMap[comment.comment_ID]); 179 | } 180 | } else { 181 | rootComments.push(commentsMap[comment.comment_ID]); 182 | } 183 | }); 184 | 185 | return rootComments; 186 | } 187 | 188 | // Generate the nested list HTML 189 | function generateNestedList(comments, document) { 190 | const ul = document.createElement("ul"); 191 | 192 | comments.forEach((comment) => { 193 | const li = document.createElement("li"); 194 | li.textContent = comment.comment_text; 195 | 196 | if (comment.children.length > 0) { 197 | li.classList.add("has-submenu"); 198 | const nestedUl = generateNestedList(comment.children, document); 199 | li.appendChild(nestedUl); 200 | } 201 | 202 | ul.appendChild(li); 203 | }); 204 | 205 | return ul; 206 | } 207 | -------------------------------------------------------------------------------- /solutions/02_04_solution/solution.js: -------------------------------------------------------------------------------- 1 | class Book { 2 | constructor(title, author, publicationYear) { 3 | this.title = title; 4 | this.author = author; 5 | this.publicationYear = publicationYear; 6 | this.status = "available"; 7 | } 8 | 9 | getDetails() { 10 | return `Title: ${this.title}, Author: ${this.author}, Publication Year: ${this.publicationYear}, Status: ${this.status}`; 11 | } 12 | } 13 | 14 | class Fiction extends Book { 15 | constructor(title, author, publicationYear, genre) { 16 | super(title, author, publicationYear); 17 | this.genre = genre; 18 | } 19 | 20 | getDetails() { 21 | return `${super.getDetails()}, Genre: ${this.genre}`; 22 | } 23 | } 24 | 25 | class NonFiction extends Book { 26 | constructor(title, author, publicationYear, subject) { 27 | super(title, author, publicationYear); 28 | this.subject = subject; 29 | } 30 | 31 | getDetails() { 32 | return `${super.getDetails()}, Subject: ${this.subject}`; 33 | } 34 | } 35 | 36 | class Library { 37 | constructor() { 38 | this.books = []; 39 | this.users = []; 40 | this.librarian = null; // single librarian 41 | } 42 | 43 | addBook(book) { 44 | this.books.push(book); 45 | // return how many books are in the library now that a new one is added 46 | return `Book "${book.title}" added. There are now ${this.books.length} books in the library.`; 47 | } 48 | 49 | removeBook(title) { 50 | // remove the book from the library if it exists. 51 | // return how many books are in the library now that one is removed 52 | if (this.books.find((book) => book.title === title)) { 53 | this.books = this.books.filter((book) => book.title !== title); 54 | return `Book "${title}" removed. There are now ${this.books.length} books in the library.`; 55 | } else { 56 | return `Book "${title}" not found.`; 57 | } 58 | } 59 | 60 | findBook(title) { 61 | // If book is found, return the book object 62 | // If book is not found, return a message 63 | const book = this.books.find((book) => book.title === title); 64 | if (book) { 65 | return `Book titled ${title} is in the library`; 66 | } else { 67 | return `Book titled "${title}" not found.`; 68 | } 69 | } 70 | } 71 | 72 | const book01 = { 73 | title: "Odyssey", 74 | author: "Homer", 75 | pubYear: 1726, 76 | genre: "Coming of Age", 77 | }; 78 | 79 | const book02 = { 80 | title: "The Yellow Wallpaper", 81 | author: "Charlotte Perkins Gilman", 82 | pubYear: 1892, 83 | genre: "Womens rights", 84 | }; 85 | 86 | const book03 = { 87 | title: "The Book of Bread", 88 | author: "Owen Simmons", 89 | pubYear: 1903, 90 | genre: "Baking", 91 | }; 92 | 93 | // Create a library with books, a librarian, and users 94 | const library = new Library(); 95 | console.log( 96 | library.addBook( 97 | new Fiction(book01.title, book01.author, book01.pubYear, book01.genre) 98 | ) 99 | ); 100 | console.log( 101 | library.addBook( 102 | new Fiction(book02.title, book02.author, book02.pubYear, book02.genre) 103 | ) 104 | ); 105 | console.log( 106 | library.addBook( 107 | new NonFiction(book03.title, book03.author, book03.pubYear, book03.genre) 108 | ) 109 | ); 110 | 111 | // Test capabilities of the library 112 | const result = () => { 113 | console.log(library.findBook("Odyssey")); // Book titled "Odyssey" is in the library 114 | console.log(library.findBook("Silo")); // Book titled "Silo" not found. 115 | console.log(library.removeBook("Odyssey")); // Book "Odyssey" removed. There are now 2 books in the library. 116 | return ""; 117 | }; 118 | 119 | console.log(result()); 120 | -------------------------------------------------------------------------------- /solutions/02_06_solution/solution.js: -------------------------------------------------------------------------------- 1 | class Book { 2 | constructor(title, author, publicationYear) { 3 | this.title = title; 4 | this.author = author; 5 | this.publicationYear = publicationYear; 6 | this.status = "available"; // Added status property 7 | } 8 | 9 | getDetails() { 10 | return `Title: ${this.title}, Author: ${this.author}, Publication Year: ${this.publicationYear}, Status: ${this.status}`; 11 | } 12 | } 13 | 14 | class Fiction extends Book { 15 | constructor(title, author, publicationYear, genre) { 16 | super(title, author, publicationYear); 17 | this.genre = genre; 18 | } 19 | 20 | getDetails() { 21 | return `${super.getDetails()}, Genre: ${this.genre}`; 22 | } 23 | } 24 | 25 | class NonFiction extends Book { 26 | constructor(title, author, publicationYear, subject) { 27 | super(title, author, publicationYear); 28 | this.subject = subject; 29 | } 30 | 31 | getDetails() { 32 | return `${super.getDetails()}, Subject: ${this.subject}`; 33 | } 34 | } 35 | 36 | class Library { 37 | constructor() { 38 | this.books = []; 39 | this.users = []; 40 | this.librarian = null; // single librarian 41 | } 42 | 43 | addBook(book) { 44 | this.books.push(book); 45 | // return how many books are in the library now that a new one is added 46 | return `Book "${book.title}" added. There are now ${this.books.length} books in the library.`; 47 | } 48 | 49 | removeBook(title) { 50 | // remove the book from the library if it exists. 51 | // return how many books are in the library now that one is removed 52 | if (this.books.find((book) => book.title === title)) { 53 | this.books = this.books.filter((book) => book.title !== title); 54 | return `Book "${title}" removed. There are now ${this.books.length} books in the library.`; 55 | } else { 56 | return `Book "${title}" not found.`; 57 | } 58 | } 59 | 60 | addUser(user) { 61 | this.users.push(user); 62 | } 63 | 64 | setLibrarian(librarian) { 65 | if (!this.librarian) { 66 | // only set if there's no librarian yet 67 | this.librarian = librarian; 68 | return "Librarian added."; 69 | } else { 70 | return "A librarian already exists."; 71 | } 72 | } 73 | 74 | findBook(title) { 75 | return this.books.find((book) => book.title === title); 76 | } 77 | 78 | findUser(name) { 79 | return this.users.find((user) => user.name === name); 80 | } 81 | } 82 | 83 | class LibraryUser { 84 | constructor(name) { 85 | this.name = name; 86 | this.borrowedBooks = []; 87 | } 88 | 89 | checkAvailability(library, title) { 90 | const book = library.findBook(title); 91 | if (book) { 92 | if (book.status === "available") { 93 | return `Book is available: ${book.getDetails()}`; 94 | } else { 95 | return `"${title}" is currently on loan.`; 96 | } 97 | } else { 98 | return `Book titled "${title}" not found.`; 99 | } 100 | } 101 | 102 | loanBook(library, title) { 103 | const librarian = library.librarian; 104 | const book = library.findBook(title); 105 | if (this.suspended) { 106 | return `${this.name} is suspended and cannot borrow books.`; 107 | } 108 | if (book) { 109 | if (book.status === "available") { 110 | // Check if book is available 111 | this.borrowedBooks.push(book); 112 | book.status = "on loan"; // Change book status 113 | return `${this.name} has borrowed the book "${title}" from ${librarian.name}.`; 114 | } else { 115 | return `"${title}" is currently on loan.`; 116 | } 117 | } else { 118 | return `Book titled "${title}" not found.`; 119 | } 120 | } 121 | } 122 | 123 | class Librarian extends LibraryUser { 124 | constructor(name) { 125 | super(name); 126 | } 127 | 128 | returnBook(library, title, userName) { 129 | const book = library.findBook(title); 130 | const user = library.findUser(userName); 131 | 132 | if (book && user) { 133 | const bookIndex = user.borrowedBooks.findIndex((b) => b.title === title); 134 | if (bookIndex !== -1) { 135 | user.borrowedBooks.splice(bookIndex, 1); 136 | book.status = "available"; // Change book status back to available 137 | return `${this.name} has checked in the book "${title}" from ${userName}.`; 138 | } else { 139 | return `"${title}" not found in user's borrowed books.`; 140 | } 141 | } else { 142 | return "Book or user not found."; 143 | } 144 | } 145 | 146 | suspendUser(library, userName) { 147 | const user = library.findUser(userName); 148 | 149 | if (user) { 150 | user.suspended = true; 151 | return `${this.name} has suspended ${userName}.`; 152 | } else { 153 | return "User not found."; 154 | } 155 | } 156 | } 157 | 158 | const book01 = { 159 | title: "Odyssey", 160 | author: "Homer", 161 | pubYear: 1726, 162 | genre: "Coming of Age", 163 | }; 164 | 165 | const book02 = { 166 | title: "The Yellow Wallpaper", 167 | author: "Charlotte Perkins Gilman", 168 | pubYear: 1892, 169 | genre: "Womens rights", 170 | }; 171 | 172 | const book03 = { 173 | title: "The Book of Bread", 174 | author: "Owen Simmons", 175 | pubYear: 1903, 176 | genre: "Baking", 177 | }; 178 | 179 | // Create a library with books, a librarian, and users 180 | const library = new Library(); 181 | library.addBook( 182 | new Fiction(book01.title, book01.author, book01.pubYear, book01.genre) 183 | ); 184 | library.addBook( 185 | new Fiction(book02.title, book02.author, book02.pubYear, book02.genre) 186 | ); 187 | library.addBook( 188 | new NonFiction(book03.title, book03.author, book03.pubYear, book03.genre) 189 | ); 190 | const librarian = new Librarian("Alice"); // only one librarian 191 | library.setLibrarian(librarian); // set the librarian 192 | const user001 = new LibraryUser("Simran"); 193 | library.addUser(user001); 194 | const user002 = new LibraryUser("Maiken"); 195 | library.addUser(user002); 196 | const librarian2 = new Librarian("Bob"); 197 | 198 | // Test capabilities of different actors 199 | const result = () => { 200 | console.log(library.setLibrarian(librarian2)); // A librarian already exists. 201 | console.log(user001.checkAvailability(library, book01.title)); // Book is available: Title: ${book01.title}, Author: ${book01.author}, Publication Year: ${book01.pubYear}, Status: available, Genre: ${book01.genre} 202 | console.log(user001.checkAvailability(library, "Silo")); // Book titled "Silo" not found. 203 | console.log(user001.loanBook(library, book01.title)); // Simran has borrowed the book "${book01.title}" from Alice. 204 | console.log(user002.loanBook(library, book01.title)); // "${book01.title}" is currently on loan. 205 | console.log(librarian.returnBook(library, book01.title, "Simran")); // Alice has checked in the book "${book02.title}" from Simran. 206 | console.log(user002.loanBook(library, book01.title)); // "${book02.title}" not found in user's borrowed books. 207 | console.log(librarian.returnBook(library, book02.title, "Maiken")); // Alice has suspended Maiken. 208 | console.log(librarian.suspendUser(library, "Maiken")); // Maiken is suspended and cannot borrow books. 209 | console.log(user002.loanBook(library, book02.title)); // Book titled "Annihilation" not found. 210 | console.log(user001.loanBook(library, "Annihilation")); // Book titled "Annihilation" not found. 211 | return "" 212 | }; 213 | 214 | console.log(result()); 215 | -------------------------------------------------------------------------------- /solutions/03_04_solution/solution.js: -------------------------------------------------------------------------------- 1 | class Logger { 2 | constructor() { 3 | if (Logger.instance == null) { 4 | this.logs = []; 5 | Logger.instance = this; 6 | } 7 | return Logger.instance; 8 | } 9 | 10 | log(message) { 11 | this.logs.push(message); 12 | } 13 | 14 | getLogCount() { 15 | return this.logs.length; 16 | } 17 | 18 | showLog() { 19 | const logResult = this.logs.map((log, index) => `${index + 1}. ${log}`); 20 | const logCount = this.getLogCount(); 21 | return { 22 | logs: logResult, 23 | logCount: logCount, 24 | }; 25 | } 26 | } 27 | 28 | const loggerInstance = new Logger(); 29 | Object.freeze(loggerInstance); 30 | 31 | const handler = { 32 | get: function (target, prop) { 33 | if (prop === "log") { 34 | return function (message) { 35 | const timestamp = new Date(); 36 | return target[prop](`${timestamp} - ${message}`); 37 | }; 38 | } else if (prop === "showLog") { 39 | return target[prop].bind(target); 40 | } else { 41 | return target[prop]; 42 | } 43 | }, 44 | }; 45 | 46 | const logger = new Proxy(loggerInstance, handler); 47 | 48 | const logEntries = [ 49 | "User Maiken created.", 50 | "User Simran created.", 51 | "User Simran updated.", 52 | "User Maiken deleted.", 53 | ]; 54 | 55 | logEntries.forEach((entry) => { 56 | logger.log(entry); 57 | }); 58 | 59 | const result = logger.showLog(); 60 | 61 | console.log(result); 62 | -------------------------------------------------------------------------------- /solutions/03_06_solution/solution.js: -------------------------------------------------------------------------------- 1 | const errorMsgs = { 2 | invalidEmail: "Not a valid email address", 3 | invalidDate: 4 | "Invalid date, please provide a date in the future and on a weekday", 5 | invalidTime: "Invalid time, please request a time between 09:00 and 16:00.", 6 | }; 7 | 8 | // ValidationManager: Manages the validation rules for various fields. 9 | class ValidationManager { 10 | constructor() { 11 | // Singleton pattern is used to ensure a single instance of ValidationManager. 12 | if (!ValidationManager.instance) { 13 | this._rules = {}; // Object to hold the validation rules. 14 | ValidationManager.instance = this; 15 | } 16 | 17 | return ValidationManager.instance; 18 | } 19 | 20 | // Method to register a validation rule. 21 | registerValidationRule(name, ruleFunction) { 22 | this._rules[name] = ruleFunction; 23 | } 24 | 25 | // Method to validate a value against a specified rule. 26 | validate(name, value) { 27 | const ruleFunction = this._rules[name]; 28 | return ruleFunction ? ruleFunction(value) : false; 29 | } 30 | } 31 | 32 | // Singleton instance of ValidationManager 33 | const validationManagerInstance = new ValidationManager(); 34 | 35 | // Booking: Represents a booking object. 36 | class Booking { 37 | constructor(firstName, lastName, email, date, time) { 38 | this.firstName = firstName; 39 | this.lastName = lastName; 40 | this.email = email; 41 | this.date = date; 42 | this.time = time; 43 | this.errors = []; // Array to hold errors if any. 44 | } 45 | } 46 | 47 | // BookingManager: Manages all the bookings. 48 | class BookingManager { 49 | constructor() { 50 | // Singleton pattern is used to ensure a single instance of BookingManager. 51 | if (!BookingManager.instance) { 52 | this.bookings = []; // Array to hold all the bookings. 53 | BookingManager.instance = this; 54 | } 55 | 56 | return BookingManager.instance; 57 | } 58 | 59 | // Method to add a booking after validating it. 60 | addBooking(booking) { 61 | const validationFields = Object.keys(bookingValidationRules); 62 | let valid = true; 63 | 64 | const errors = booking.errors; 65 | 66 | // Validate each field in the booking. 67 | for (const field of validationFields) { 68 | const validationRule = bookingValidationRules[field]; 69 | const fieldValue = booking[field]; 70 | 71 | if (!validationManagerInstance.validate(validationRule, fieldValue)) { 72 | // Add corresponding error message based on the field. 73 | switch (field) { 74 | case "email": 75 | errors.push(errorMsgs.invalidEmail); 76 | break; 77 | case "date": 78 | errors.push(errorMsgs.invalidDate); 79 | break; 80 | case "time": 81 | errors.push(errorMsgs.invalidTime); 82 | break; 83 | default: 84 | errors.push(`Invalid ${field}`); 85 | } 86 | 87 | valid = false; 88 | } 89 | } 90 | 91 | // Check if there is already a booking at the same date and time. 92 | const overlappingBooking = this.bookings.find( 93 | (existingBooking) => 94 | existingBooking.date === booking.date && 95 | existingBooking.time === booking.time 96 | ); 97 | 98 | if (overlappingBooking) { 99 | booking.errors.push("Time not available"); 100 | valid = false; 101 | } else { 102 | this.bookings.push(booking); 103 | } 104 | 105 | return valid; 106 | } 107 | } 108 | 109 | // Singleton instance of BookingManager 110 | const bookingManagerInstance = new BookingManager(); 111 | 112 | // Function to create a validation proxy for a given object and a set of rules. 113 | function createValidationProxy(object, rules) { 114 | return new Proxy(object, { 115 | set(target, prop, value) { 116 | // If the property being set is in the rules and the value is invalid, throw an error. 117 | if ( 118 | prop in rules && 119 | !validationManagerInstance.validate(rules[prop], value) 120 | ) { 121 | throw new Error(`Invalid ${prop}`); 122 | } 123 | 124 | // Otherwise, set the value as usual. 125 | target[prop] = value; 126 | return true; 127 | }, 128 | }); 129 | } 130 | 131 | // Validation rules 132 | const bookingValidationRules = { 133 | email: "email", 134 | date: "bookingDate", 135 | time: "bookingTime", 136 | }; 137 | 138 | // Email validation 139 | validationManagerInstance.registerValidationRule("email", (value) => { 140 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 141 | return typeof value === "string" && emailRegex.test(value); 142 | }); 143 | 144 | // Date validation 145 | validationManagerInstance.registerValidationRule("bookingDate", (value) => { 146 | const dateRegex = /^\d{4}-\d{2}-\d{2}$/; 147 | if (!dateRegex.test(value)) return false; 148 | 149 | const date = new Date(value); 150 | const today = new Date(); 151 | today.setHours(0, 0, 0, 0); 152 | 153 | const day = date.getDay(); 154 | return date >= today && day >= 1 && day <= 5; // Weekdays only 155 | }); 156 | 157 | // Time validation 158 | validationManagerInstance.registerValidationRule("bookingTime", (value) => { 159 | const timeRegex = /^([0-9]{2}):([0-9]{2})$/; 160 | if (!timeRegex.test(value)) return false; 161 | 162 | const [hour, minute] = value.split(":").map((x) => parseInt(x)); 163 | return hour >= 9 && hour < 16 && minute === 0; 164 | }); 165 | 166 | const bookings = [ 167 | { 168 | firstName: "Amelia", 169 | lastName: "Zhang", 170 | email: "amelia.zhang@example.com", 171 | date: "2023-06-01", 172 | time: "09:00", 173 | }, 174 | { 175 | firstName: "111", 176 | lastName: "García", 177 | email: "sergio.garcia@example.com", 178 | date: "2023-06-01", 179 | time: "09:00", 180 | }, 181 | { 182 | firstName: "Fatima", 183 | lastName: "Hassan", 184 | email: "fatima.hassan@example.com", 185 | date: "2023-06-01", 186 | time: "08:00", 187 | }, 188 | { 189 | firstName: "Kwame", 190 | lastName: "Nkrumah", 191 | email: "kwame.nkrumah@examplecom", 192 | date: "2023-06-03", 193 | time: "10:00", 194 | }, 195 | { 196 | firstName: "Maiken", 197 | lastName: "Galden", 198 | email: "maiken@example.com", 199 | date: "2023-07-05", 200 | time: "10:00", 201 | }, 202 | ]; 203 | 204 | // This function returns an array of results of booking attempts 205 | const result = bookings.map((booking, index) => { 206 | try { 207 | const testBooking = new Booking( 208 | booking.firstName, 209 | booking.lastName, 210 | booking.email, 211 | booking.date, 212 | booking.time 213 | ); 214 | const validatedBooking = createValidationProxy( 215 | testBooking, 216 | bookingValidationRules 217 | ); 218 | 219 | if (bookingManagerInstance.addBooking(validatedBooking)) { 220 | return `Booking ${index + 1} added successfully`; 221 | } else { 222 | return `Booking ${ 223 | index + 1 224 | } failed with the following errors: ${testBooking.errors.join(". ")}`; 225 | } 226 | } catch (error) { 227 | return `Booking ${index + 1} error: ${error.message}`; 228 | } 229 | }); 230 | 231 | console.log(result); 232 | -------------------------------------------------------------------------------- /solutions/04_04_solution/solution.js: -------------------------------------------------------------------------------- 1 | class WeatherStation { 2 | constructor() { 3 | this.observers = new Set(); 4 | this.temperature = null; 5 | this.displayNames = new Map(); 6 | } 7 | 8 | addObserver(observer, displayName) { 9 | if (!this.observers.has(observer)) { 10 | this.observers.add(observer); 11 | this.displayNames.set(observer, displayName); 12 | return `Weather station: ${observer.displayName} subscribed to the weather station.`; 13 | } 14 | } 15 | 16 | removeObserver(observer) { 17 | if (this.observers.has(observer)) { 18 | this.observers.delete(observer); 19 | this.displayNames.delete(observer); 20 | return `Weather station: ${observer.displayName} unsubscribed from the weather station.`; 21 | } 22 | } 23 | 24 | notifyObservers() { 25 | this.observers.forEach((observer) => { 26 | const displayName = this.displayNames.get(observer); 27 | observer.update(this.temperature, displayName); 28 | }); 29 | } 30 | 31 | setTemperature(newTemperature) { 32 | this.temperature = newTemperature; 33 | this.notifyObservers(); 34 | return `Weather station: New temperature is ${newTemperature}°C`; 35 | } 36 | } 37 | 38 | class WeatherDisplay { 39 | constructor(displayName) { 40 | this.displayName = displayName; 41 | } 42 | 43 | update(temperature, displayName) { 44 | return `Weather display ${displayName} shows temperature: ${temperature}°C`; 45 | } 46 | } 47 | 48 | function runWeatherStation() { 49 | // Create weather station and displays 50 | const weatherStation = new WeatherStation(); 51 | const display1 = new WeatherDisplay("Display 1"); 52 | const display2 = new WeatherDisplay("Display 2"); 53 | const results = []; 54 | 55 | // Subscribe displays to weather station 56 | results.push(weatherStation.addObserver(display1, display1.displayName)); 57 | results.push(weatherStation.addObserver(display2, display2.displayName)); 58 | // Set temperature and notify observers 59 | results.push(weatherStation.setTemperature(25)); 60 | results.push(weatherStation.setTemperature(28)); 61 | // Unsubscribe display 1 62 | results.push(weatherStation.removeObserver(display1)); 63 | results.push(weatherStation.setTemperature(22)); 64 | results.push(weatherStation.setTemperature(58)); 65 | 66 | return results; 67 | } 68 | 69 | const results = runWeatherStation(); 70 | console.log(results); 71 | -------------------------------------------------------------------------------- /solutions/04_06_solution/explanation.md: -------------------------------------------------------------------------------- 1 | This script showcases a simple implementation of the Observer design pattern in JavaScript. It has a few key classes and concepts: 2 | 3 | 1. `Observable`: This is the base class for classes that wish to provide a subscription-based model for notifications. It manages a list of observers and provides methods to add, remove, and notify these observers. The `notify` method is used to inform all subscribed observers of an event or change in state, and it does this by calling each observer's `update` method with the necessary data. 4 | 5 | 2. `Stock`: This class extends `Observable` to represent a stock with a name and price. It provides an `updatePrice` method that changes the price of the stock, validates the input, and notifies all observers of this price change. 6 | 7 | 3. Observer Classes (`PriceDisplay`, `PriceThresholdNotifier`, and `PercentageChangeNotifier`): Each of these classes has an `update` method, which is called when the `Stock` object's price changes. These methods define different behaviors based on the updated price: 8 | - `PriceDisplay` just logs the new price to the `reports` array. 9 | - `PriceThresholdNotifier` checks if the new price exceeds a pre-set threshold and logs a message to the `reports` array if so. 10 | - `PercentageChangeNotifier` calculates the percentage change in price from the previous update, and logs a message to the `reports` array if this change exceeds a certain percentage. 11 | 12 | In the main script, a `Stock` object is created along with several observer objects. The observers are added to the stock, the stock's price is updated several times using an array of prices, some observers are then removed, and the price is updated again. The resulting `reports` array, which has been populated by the observer classes, is then logged to the console. 13 | 14 | This example showcases the power of the Observer design pattern for creating decoupled systems. The `Stock` class doesn't need to know anything about what the observers do with the price updates - it just sends the updates out. Similarly, each observer class operates independently of the others. This makes the system flexible and easy to extend. For instance, we could add new types of observer classes without needing to modify the `Stock` or `Observable` classes at all. 15 | -------------------------------------------------------------------------------- /solutions/04_06_solution/solution.js: -------------------------------------------------------------------------------- 1 | // Empty array to store reports 2 | let reports = []; 3 | 4 | // The main Observable class 5 | class Observable { 6 | constructor() { 7 | // Initialize an empty array to store observers 8 | this.observers = []; 9 | } 10 | 11 | // Method to add an observer to the observers array 12 | addObserver(observer) { 13 | this.observers.push(observer); 14 | } 15 | 16 | // Method to remove an observer from the observers array 17 | removeObserver(observer) { 18 | // Find the index of the observer in the observers array 19 | const index = this.observers.indexOf(observer); 20 | // If the observer is found, remove it from the array 21 | if (index !== -1) { 22 | this.observers.splice(index, 1); 23 | } 24 | } 25 | 26 | // Method to notify all observers with some data 27 | notify(data) { 28 | // Call the update method on each observer with the data 29 | this.observers.forEach((observer) => observer.update(data)); 30 | } 31 | } 32 | 33 | // Class called Stock that extends Observable 34 | class Stock extends Observable { 35 | constructor(name) { 36 | // Call the constructor of the Observable class 37 | super(); 38 | // Set the name of the stock 39 | this.name = name; 40 | // Set the initial price of the stock to 0 41 | this.price = 0; 42 | } 43 | 44 | // Method to update the price of the stock 45 | updatePrice(price) { 46 | // If the price is not a number, add an error report to the reports array and return 47 | if (typeof price !== "number") { 48 | reports.push( 49 | `Error: Invalid price data '${price}' for ${this.name}. Price must be a number.` 50 | ); 51 | return; 52 | } 53 | // Set the price of the stock to the new price 54 | this.price = price; 55 | // Notify all observers with an object containing the name and price of the stock 56 | this.notify({ name: this.name, price: this.price }); 57 | } 58 | } 59 | 60 | // Class called PriceDisplay 61 | class PriceDisplay { 62 | // Method to update the price display with new data 63 | update(data) { 64 | // Add a report to the reports array with the name and price of the stock 65 | reports.push( 66 | `Price Display: ${data.name} stock price is now $${data.price}` 67 | ); 68 | } 69 | } 70 | 71 | // Class called PriceThresholdNotifier 72 | class PriceThresholdNotifier { 73 | constructor(threshold) { 74 | // Set the threshold for the notifier 75 | this.threshold = threshold; 76 | } 77 | 78 | // Method to notify if the price of the stock has reached the threshold 79 | update(data) { 80 | // If the price of the stock is greater than or equal to the threshold, add a report to the reports array 81 | if (data.price >= this.threshold) { 82 | reports.push( 83 | `Price Threshold Notifier: ${data.name} stock price has reached the threshold of $${this.threshold}` 84 | ); 85 | } 86 | } 87 | } 88 | 89 | // Class called PercentageChangeNotifier 90 | class PercentageChangeNotifier { 91 | constructor(percentage) { 92 | // Set the percentage change threshold for the notifier 93 | this.percentage = percentage; 94 | // Set the previous price of the stock to null 95 | this.previousPrice = null; 96 | } 97 | 98 | // Method to notify if the price of the stock has changed by a certain percentage 99 | update(data) { 100 | // If the previous price is null, set it to the current price and return 101 | if (this.previousPrice === null) { 102 | this.previousPrice = data.price; 103 | return; 104 | } 105 | 106 | // Calculate the percentage change in the price of the stock 107 | const change = 108 | ((data.price - this.previousPrice) / this.previousPrice) * 100; 109 | // If the absolute value of the percentage change is greater than or equal to the threshold, add a report to the reports array 110 | if (Math.abs(change) >= this.percentage) { 111 | reports.push( 112 | `Percentage Change Notifier: ${ 113 | data.name 114 | } stock price has changed by ${change.toFixed(2)}%` 115 | ); 116 | } 117 | // Set the previous price of the stock to the current price 118 | this.previousPrice = data.price; 119 | } 120 | } 121 | 122 | // New instance of the Stock class with the name "ABC" 123 | const stock = new Stock("ABC"); 124 | 125 | // Instances of the PriceDisplay, PriceThresholdNotifier, and PercentageChangeNotifier classes 126 | const priceDisplay = new PriceDisplay(); 127 | const priceThresholdNotifier = new PriceThresholdNotifier(150); 128 | const percentageChangeNotifier = new PercentageChangeNotifier(5); 129 | 130 | // Add the PriceDisplay, PriceThresholdNotifier, and PercentageChangeNotifier instances as observers of the stock instance 131 | stock.addObserver(priceDisplay); 132 | stock.addObserver(priceThresholdNotifier); 133 | stock.addObserver(percentageChangeNotifier); 134 | 135 | // Array of prices to update the stock with 136 | const priceArray = [100, 102, 151, -100, "aaa", "200"]; 137 | 138 | // Update the stock with each price in the priceArray 139 | priceArray.forEach((price) => stock.updatePrice(price)); 140 | 141 | // Remove the PriceThresholdNotifier and PercentageChangeNotifier instances as observers of the stock instance 142 | stock.removeObserver(priceThresholdNotifier); 143 | stock.removeObserver(percentageChangeNotifier); 144 | 145 | // Update the stock with each price in the priceArray again 146 | priceArray.forEach((price) => stock.updatePrice(price)); 147 | 148 | // Log the reports array to the console 149 | console.log(reports); 150 | -------------------------------------------------------------------------------- /solutions/05_04_solution/solution.js: -------------------------------------------------------------------------------- 1 | const runningTally = []; 2 | 3 | // Reactive Object 4 | class ReactiveObject { 5 | constructor() { 6 | this.subscribers = []; 7 | } 8 | 9 | // Subscribe to changes 10 | subscribe(callback) { 11 | // Add callback to subscribers 12 | this.subscribers.push(callback); 13 | } 14 | 15 | // Unsubscribe from changes 16 | unsubscribe(callback) { 17 | // Remove the callback from subscribers 18 | this.subscribers = this.subscribers.filter((sub) => sub !== callback); 19 | } 20 | 21 | updateSubscribers() { 22 | this.subscribers.forEach((callback) => callback(this)); 23 | } 24 | } 25 | 26 | const GST_RATE = 0.05; // 5% GST 27 | const PST_RATE = 0.07; // 7% PST 28 | 29 | class ShoppingCart extends ReactiveObject { 30 | constructor() { 31 | super(); 32 | this.items = []; 33 | this.totalBeforeTax = 0; 34 | this.gst = 0; 35 | this.pst = 0; 36 | this.sumTotal = 0; 37 | } 38 | 39 | addItem(item) { 40 | this.items.push(item); 41 | this.#updateTotals(); 42 | this.updateSubscribers(); 43 | } 44 | 45 | removeItem(index) { 46 | if (index < 0 || index >= this.items.length) { 47 | throw new Error("Invalid index"); 48 | } 49 | this.items.splice(index, 1); 50 | this.#updateTotals(); 51 | this.updateSubscribers(); 52 | } 53 | 54 | #updateTotals() { 55 | this.totalBeforeTax = this.items.reduce( 56 | (total, item) => total + item.price, 57 | 0 58 | ); 59 | this.gst = this.totalBeforeTax * GST_RATE; 60 | this.pst = this.totalBeforeTax * PST_RATE; 61 | this.sumTotal = this.totalBeforeTax + this.gst + this.pst; 62 | } 63 | } 64 | 65 | // Usage example 66 | const cart = new ShoppingCart(); 67 | 68 | const tallyCart = (cart) => { 69 | const tally = { 70 | totalBeforeTax: cart.totalBeforeTax.toFixed(2), 71 | gst: cart.gst.toFixed(2), 72 | pst: cart.pst.toFixed(2), 73 | sumTotal: cart.sumTotal.toFixed(2), 74 | }; 75 | 76 | runningTally.push(tally); 77 | }; 78 | 79 | // Subscribe to changes 80 | cart.subscribe(tallyCart); 81 | 82 | // Sample items 83 | const itemArray = [ 84 | { name: "Item 1", price: 19.99 }, 85 | { name: "Item 2", price: 29.99 }, 86 | { name: "Item 3", price: 5.29 }, 87 | { name: "Item 4", price: 9.99 }, 88 | ]; 89 | 90 | // Iterate through itemArray and add items to the cart 91 | itemArray.forEach((item) => cart.addItem(item)); 92 | 93 | // Remove first item from the cart 94 | try { 95 | cart.removeItem(0); 96 | } catch (error) { 97 | console.error(error); 98 | } 99 | 100 | // Unsubscribe from changes 101 | cart.unsubscribe(tallyCart); 102 | 103 | // These items should not be counted. 104 | itemArray.forEach((item) => cart.addItem(item)); 105 | 106 | console.log(JSON.stringify(runningTally, null, 2)); 107 | console.log(runningTally.length); 108 | -------------------------------------------------------------------------------- /solutions/05_06_solution/solution.js: -------------------------------------------------------------------------------- 1 | // Reactive Object 2 | class ReactiveObject { 3 | constructor() { 4 | this.subscribers = []; 5 | } 6 | 7 | subscribe(callback) { 8 | this.subscribers.push(callback); 9 | } 10 | 11 | updateSubscribers(data, action) { 12 | this.subscribers.forEach((callback) => callback(data, action)); 13 | } 14 | } 15 | 16 | class PlantTracker extends ReactiveObject { 17 | constructor() { 18 | super(); 19 | this.plants = []; 20 | } 21 | 22 | sowPlant(name, sowingDate, estimatedFruitingTime) { 23 | const plant = { 24 | name, 25 | sowingDate, 26 | estimatedFruitingTime, 27 | actualFruitingTime: null, 28 | difference: null, 29 | }; 30 | 31 | this.plants.push(plant); 32 | this.updateSubscribers(plant, "sow"); 33 | } 34 | 35 | recordActualFruitingTime(plantIndex, actualFruitingTime) { 36 | const plant = this.plants[plantIndex]; 37 | plant.actualFruitingTime = actualFruitingTime; 38 | 39 | const sowingDate = new Date(plant.sowingDate); 40 | const estimatedFruitingDate = new Date( 41 | sowingDate.getTime() + plant.estimatedFruitingTime * 24 * 60 * 60 * 1000 42 | ); 43 | plant.difference = Math.round( 44 | (new Date(actualFruitingTime) - estimatedFruitingDate) / 45 | (24 * 60 * 60 * 1000) 46 | ); 47 | 48 | this.updateSubscribers(plant, "fruit"); 49 | } 50 | } 51 | 52 | // Usage example 53 | const plantTracker = new PlantTracker(); 54 | 55 | plantTracker.subscribe((plant, action) => { 56 | if (action === "sow") { 57 | console.log("New plant:"); 58 | } else if (action === "fruit") { 59 | console.log("Fruited plant:"); 60 | } 61 | console.log(plant); 62 | console.log(""); 63 | }); 64 | 65 | const plantArray = [ 66 | { 67 | name: "Tomato", 68 | sowingDate: "2023-02-01", 69 | estimatedFruitingTime: 60, 70 | actualFruitingTime: "2023-04-10", 71 | }, 72 | { 73 | name: "Cucumber", 74 | sowingDate: "2023-03-21", 75 | estimatedFruitingTime: 50, 76 | actualFruitingTime: "2023-06-20", 77 | }, 78 | { 79 | name: "Pepper", 80 | sowingDate: "2023-04-17", 81 | estimatedFruitingTime: 70, 82 | actualFruitingTime: "2023-06-20", 83 | }, 84 | { 85 | name: "Eggplant", 86 | sowingDate: "2023-05-01", 87 | estimatedFruitingTime: 80, 88 | actualFruitingTime: "2023-07-20", 89 | }, 90 | ]; 91 | 92 | plantArray.forEach((plant) => { 93 | plantTracker.sowPlant( 94 | plant.name, 95 | plant.sowingDate, 96 | plant.estimatedFruitingTime 97 | ); 98 | plantTracker.recordActualFruitingTime( 99 | plantArray.indexOf(plant), 100 | plant.actualFruitingTime 101 | ); 102 | }); 103 | 104 | console.log(plantTracker.plants); 105 | --------------------------------------------------------------------------------