DEV Community

Cover image for Modern JavaScript Essentials: From Basics to Asynchronous Programming
Preyum Kumar
Preyum Kumar

Posted on

Modern JavaScript Essentials: From Basics to Asynchronous Programming

In an AI-driven world, a strong foundation in core technologies is essential. To keep my skills sharp, I'm starting a regular blog series covering the tech stack I use. Welcome to the first post in my Next.js journey, where we'll start by mastering the JavaScript fundamentals.

JavaScript

Index

  1. Index
  2. How to Install JavaScript
  3. Putting Comments in the Code
  4. The Variables
  5. The DataTypes
  6. Operators, Shorthand and Semicolon
  7. Ternary Operator and Short-Circuit Evaluation
  8. The Escape Character and Strings
  9. For, While, Do While and Switch
  10. Array Methods (map, filter, reduce)
  11. Regular, Anonymous and Arrow Functions
  12. Closures
  13. Function Arguments
  14. Spread Operator
  15. Rest Parameters
  16. Destructuring object values for assignment
  17. Object Methods
  18. Optional Chaining and Nullish Coalescing
  19. Backtick for Strings with values
  20. Getters and Setters
  21. Asynchronous JavaScript
  22. The setTimeout() Function
  23. Callbacks
  24. Promises
  25. Async and Await
  26. Modules (Import and Export)

How to Install JavaScript

  • We don't install javascript. If we have any internet browser, there is support for JavaScript there.
  • Chrome/Safari/Firefox all have javascript support.
  • Along with browsers we can also use platforms like codepen.io or platforms like scrimba.com

Putting Comments in the Code

  • To put comment in the code we use //
let val = "apple" // It is a comment
Enter fullscreen mode Exit fullscreen mode
  • For multiline comment we can use /* */
let fruit = "apple"
/* This is a 
    Multiline
    Comment
*/
let number = 9
Enter fullscreen mode Exit fullscreen mode

The Variables

  • JavaScript has three types of variables var, let and const.
  • var can be used to define a variable and the values can be redefined. It was used mainly in the past as it was the only option but now a days it is not intended to be used normally. It is because it has issues like ignoring block scope, and getting attached to the window scope in global scope. var has function scope meaning if defined inside a function it will be remembered only inside that function. var variables can also be redeclared which can cause silent bugs.
var fruitName = "Apple";
var fruitName = "Mango"; // Redeclaration is allowed (can cause bugs)
fruitName = "Banana"; // Reassignment is allowed
console.log(fruitName); // "Banana"

if (true) {
    var blockVar = "I ignore block scope!";
}
console.log(blockVar); // "I ignore block scope!" (Accessible outside the block)
Enter fullscreen mode Exit fullscreen mode
  • Implicit global variable creation - Recipe for bugs: If you assign a value to a variable without declaring it with var, let, or const, JavaScript automatically makes it a global variable (attached to the window object). This can lead to unexpected overwrites and hard-to-trace bugs.
function createBug() {
    // Implicit global variable creation
    rogueFruit = "Mango"; 
}
createBug();
console.log(rogueFruit); // Outputs "Mango" even outside the function!
Enter fullscreen mode Exit fullscreen mode
  • let is similar as var but with block scoping. Also in case of hoisting var is set as undefined while let and const can go to TDZ (Temporal Dead Zone) meaning they will have no value and get Reference Error if accessed. let does not allow redeclaration of the same variable and so does const.
let myName = "Preyum Kumar";
myName = "Preyum"; // Allowed: Reassignment
// let myName = "Kumar"; // Error: Cannot redeclare block-scoped variable
Enter fullscreen mode Exit fullscreen mode
  • const is used when we want to define a variable only once and freeze its reference. Strings and numbers defined as const are locked in while arrays can be inserted with more values as only their reference is constant. const needs to be defined at the time of declaration unlike the other two variables.
const favoriteFruit = "Apple";
// favoriteFruit = "Banana"; // TypeError: Assignment to constant variable.

const fruitBasket = ["Apple"];
fruitBasket.push("Orange"); // Allowed: We are modifying the array, not the reference.
console.log(fruitBasket); // ["Apple", "Orange"]
Enter fullscreen mode Exit fullscreen mode

The DataTypes

  • undefined: Represents a variable that has been declared but not yet assigned a value.
  • number: Represents both integer and floating-point numbers.
  • boolean: Represents a logical entity and can have two values: true or false.
  • string: Represents a sequence of characters used to represent text.
  • null: Represents the intentional absence of any object value.
  • object: Represents a collection of properties (key-value pairs) or complex data structures (like arrays, functions).
  • symbol: Represents a unique and immutable primitive value, often used as object property keys.
  • bigint: Represents whole numbers larger than the maximum safe integer (Number.MAX_SAFE_INTEGER, which is 9007199254740991 or 2^53 - 1).
let notAssigned;
console.log(typeof notAssigned); // "undefined"

let age = 25;
console.log(typeof age); // "number"

let isComputerScienceExpert = true;
console.log(typeof isComputerScienceExpert); // "boolean"

let name = "Preyum Kumar";
console.log(typeof name); // "string"

let emptyValue = null;
console.log(typeof emptyValue); // "object" (This is a known quirk in JavaScript!)

let person = { name: "Preyum", domain: "Computer Science" };
console.log(typeof person); // "object"

let uniqueId = Symbol("id");
console.log(typeof uniqueId); // "symbol"

// The 'n' at the end tells JavaScript to treat this as a BigInt
let largeNumber = 9007199254740992n; // 1 larger than MAX_SAFE_INTEGER
console.log(typeof largeNumber); // "bigint"
Enter fullscreen mode Exit fullscreen mode

Operators, Shorthand and Semicolon

  • The lines are recognizable with or without semicolon. Semicolons are optional but recommended for clarity.
let fruit1 = "Apple"
let fruit2 = "Orange"; // Both lines are valid
Enter fullscreen mode Exit fullscreen mode
  • Operators +, -, /, %, <, >, <=, >=, *, &&, || etc and their shorthand use:
let total = 10 + 5; // 15
let diff = 10 - 5; // 5
let mult = 10 * 5; // 50
let div = 10 / 5; // 2
let remainder = 10 % 3; // 1

// Comparison
console.log(10 > 5); // true
console.log(10 <= 10); // true

// Logical
console.log(true && false); // false
console.log(true || false); // true

// Shorthand assignments
let count = 5;
count += 5; // Equivalent to: count = count + 5 (Result: 10)
count *= 2; // Equivalent to: count = count * 2 (Result: 20)
Enter fullscreen mode Exit fullscreen mode
  • Operators like == and === also != and !==:
    • == checks for value equality (with type coercion).
    • === checks for strict equality (both value and type must match).
console.log(5 == "5"); // true (Type coercion happens)
console.log(5 === "5"); // false (Different types: Number vs String)
console.log(5 != "5"); // false
console.log(5 !== "5"); // true
Enter fullscreen mode Exit fullscreen mode
  • Also incrementing and decrementing shorthand like ++, --:
let step = 1;
step++; // Increments step by 1 (Result: 2)
step--; // Decrements step by 1 (Result: 1)
Enter fullscreen mode Exit fullscreen mode

Ternary Operator and Short-Circuit Evaluation

  • Ternary Operator: A concise way to write an if-else statement. It takes three operands: a condition followed by a question mark (?), an expression to execute if truthy, and a colon (:), followed by an expression to execute if falsy. Heavily used in React for conditional rendering.
let age = 20;
// Condition ? exprIfTrue : exprIfFalse
let canVote = age >= 18 ? "Yes" : "No";
console.log(canVote); // "Yes"
Enter fullscreen mode Exit fullscreen mode
  • Short-Circuit Evaluation (&& and ||):
    • && (Logical AND): Returns the first falsy value, or the last truthy value if all are truthy. Very commonly used in React to render components conditionally (e.g., isLoggedIn && <Dashboard />).
    • || (Logical OR): Returns the first truthy value, or the last falsy value if all are falsy. Useful for assigning default values.
let isLoggedIn = true;
// The second part only executes if the first part is true
isLoggedIn && console.log("User dashboard rendered");

let userTheme = null;
let defaultTheme = "dark";
// If userTheme is falsy (like null), it falls back to "dark"
let activeTheme = userTheme || defaultTheme;
console.log(activeTheme); // "dark"
Enter fullscreen mode Exit fullscreen mode

The Escape Character and Strings

  • Example of string with "" and inside also double quotes:
let quote = "Preyum Kumar said, \"Computer Vision is fascinating!\"";
Enter fullscreen mode Exit fullscreen mode
  • Other ways like using single quote to cover double quotes:
let singleQuoteStr = 'Preyum Kumar said, "NLP is amazing!"';
Enter fullscreen mode Exit fullscreen mode
  • Other way to use back ticks `` to put both single and double quotes inside:
let backtickStr = `Preyum's favorite subject is "Computer Science".`;
Enter fullscreen mode Exit fullscreen mode
  • To put backslash itself we use two backslashes:
let path = "C:\\Users\\PreyumKumar\\Documents";
Enter fullscreen mode Exit fullscreen mode

For, While, Do While and Switch

  • For Loop: Repeats a block of code a specific number of times.
let fruits = ["Apple", "Mango", "Banana"];
for (let i = 0; i < fruits.length; i++) {
    console.log(fruits[i]);
}
Enter fullscreen mode Exit fullscreen mode
  • While Loop: Executes a block of code as long as a specified condition is true.
let count = 0;
while (count < 3) {
    console.log("Count is: " + count);
    count++;
}
Enter fullscreen mode Exit fullscreen mode
  • Do...While Loop: Similar to a while loop, but it executes the block of code at least once before checking the condition.
let runs = 0;
do {
    console.log("This runs at least once!");
    runs++;
} while (runs < 0);
Enter fullscreen mode Exit fullscreen mode
  • Switch Statement: Evaluates an expression, matching the expression's value to a case clause, and executes statements associated with that case.
let chosenFruit = "Apple";

switch (chosenFruit) {
    case "Banana":
        console.log("You chose Banana.");
        break;
    case "Apple":
        console.log("You chose Apple.");
        break;
    default:
        console.log("Unknown fruit.");
}
Enter fullscreen mode Exit fullscreen mode

Array Methods (map, filter, reduce)

  • map(): Creates a new array populated with the results of calling a provided function on every element in the calling array. This is the primary way to render lists of elements in React.
let numbers = [1, 2, 3];
let doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode
  • filter(): Creates a shallow copy of a portion of a given array, filtered down to just the elements from the given array that pass the test implemented by the provided function. Constantly used for manipulating state arrays, like deleting an item from a list.
let ages = [15, 21, 18, 12, 30];
let adults = ages.filter(age => age >= 18);
console.log(adults); // [21, 18, 30]
Enter fullscreen mode Exit fullscreen mode
  • reduce(): Executes a user-supplied "reducer" callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value.
let prices = [10, 20, 30];
let totalCost = prices.reduce((acc, current) => acc + current, 0);
console.log(totalCost); // 60
Enter fullscreen mode Exit fullscreen mode

Regular, Anonymous and Arrow Functions

  • Regular Functions: Declared using the function keyword. They have their own this context.
function greet(name) {
    return "Hello, " + name;
}
console.log(greet("Preyum Kumar"));
Enter fullscreen mode Exit fullscreen mode
  • Anonymous Functions: Functions without a name, often assigned to variables or passed as callbacks (discussed in detail later).
let sayGoodbye = function(name) {
    return "Goodbye, " + name;
};
Enter fullscreen mode Exit fullscreen mode
  • Arrow Functions: A concise syntax for writing functions. They do not have their own this binding (they inherit it from the parent scope).
    • Explicit Return: When using curly braces {}, you must explicitly use the return keyword to return a value.
    • Implicit Return: If you omit the curly braces, the arrow function implicitly returns the single evaluated expression. You can wrap the expression in parentheses () for multi-line implicit returns, which is very common in React JSX.
// Explicit Return (uses {})
const greetArrow = (name) => {
    return "Hello, " + name;
};

// Implicit Return (no {}, evaluates the expression on the same line)
const greetShort = name => "Hello, " + name;

// Implicit Return with Parentheses (useful for returning objects)
const getUser = (name) => ({
    name: name,
    role: "Admin"
});
Enter fullscreen mode Exit fullscreen mode

Closures

  • A Closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function's variables—a scope chain. Closures are a fundamental concept that explains how React Hooks (like useState and useEffect) "remember" values between renders.
function makeCounter() {
    let count = 0; // count is a local variable created by makeCounter

    return function() { // The inner function is a closure
        count++; // It has access to count from the outer function
        return count;
    };
}

let myCounter = makeCounter();
console.log(myCounter()); // 1
console.log(myCounter()); // 2
console.log(myCounter()); // 3
Enter fullscreen mode Exit fullscreen mode

Function Arguments

  • Default Arguments use case: We can assign default values to parameters to ensure the function works even if some arguments are missing.
function makeJuice(fruit1, fruit2 = "Water") {
    return `Making juice with ${fruit1} and ${fruit2}`;
}
console.log(makeJuice("Apple")); // "Making juice with Apple and Water"
Enter fullscreen mode Exit fullscreen mode
  • Order in which default arguments must be: Default arguments must be placed at the end of the parameter list. If they are placed first, JavaScript won't know which arguments to skip when fewer arguments are passed.
// WRONG: function wrongOrder(fruit1 = "Apple", fruit2) { ... }
// CORRECT:
function correctOrder(fruit1, fruit2 = "Banana") {
    console.log(fruit1, fruit2);
}
Enter fullscreen mode Exit fullscreen mode

Spread Operator

  • The spread operator (...) allows an iterable (like an array or string) to be expanded in places where zero or more arguments or elements are expected, or an object expression to be expanded.
  • How spread operator is used to actually copy array:
let originalFruits = ["Apple", "Mango"];
// let badCopy = originalFruits; // This just copies the reference!

let goodCopy = [...originalFruits]; // Creates a true, independent copy
goodCopy.push("Banana");

console.log(originalFruits); // ["Apple", "Mango"]
console.log(goodCopy); // ["Apple", "Mango", "Banana"]
Enter fullscreen mode Exit fullscreen mode

Rest Parameters

  • The Rest Parameter syntax (...args) allows a function to accept an indefinite number of arguments as an array. While the spread operator expands iterables into individual elements, the rest parameter collects multiple elements and condenses them into a single array.
function calculateSum(...numbers) {
    // numbers is an array containing all passed arguments
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(calculateSum(1, 2, 3)); // 6
console.log(calculateSum(10, 20, 30, 40, 50)); // 150
Enter fullscreen mode Exit fullscreen mode

Destructuring object values for assignment

  • Destructuring makes it possible to unpack values from arrays, or properties from objects, into distinct variables.
  • Destructing single layer example:
let user = {
    name: "Preyum Kumar",
    domain: "Computer Science"
};

let { name, domain } = user;
console.log(name); // "Preyum Kumar"
Enter fullscreen mode Exit fullscreen mode
  • Destructuring nested object for assignment:
let expertUser = {
    profile: {
        firstName: "Preyum",
        expertise: "Computer Vision"
    }
};

let { profile: { firstName, expertise } } = expertUser;
console.log(expertise); // "Computer Vision"
Enter fullscreen mode Exit fullscreen mode

Object Methods

  • JavaScript provides several built-in methods to work with objects, returning arrays of their keys, values, or entries (key-value pairs).
  • Object.keys(): Returns an array of a given object's own enumerable string-keyed property names.
  • Object.values(): Returns an array of a given object's own enumerable string-keyed property values.
  • Object.entries(): Returns an array of a given object's own enumerable string-keyed property [key, value] pairs.
let user = {
    name: "Preyum Kumar",
    domain: "Computer Science",
    expertise: "Computer Vision"
};

console.log(Object.keys(user)); 
// ["name", "domain", "expertise"]

console.log(Object.values(user)); 
// ["Preyum Kumar", "Computer Science", "Computer Vision"]

console.log(Object.entries(user)); 
// [["name", "Preyum Kumar"], ["domain", "Computer Science"], ["expertise", "Computer Vision"]]
Enter fullscreen mode Exit fullscreen mode

Optional Chaining and Nullish Coalescing

  • Optional Chaining (?.): Enables reading the value of a property located deep within a chain of connected objects without having to check that each reference in the chain is valid. It returns undefined if the reference is nullish (null or undefined) instead of throwing an error. Very helpful when dealing with API data in Next.js.
let userProfile = {
    name: "Preyum Kumar",
    // address is missing here
};

// Trying to access userProfile.address.city directly would throw an error.
// Using ?. safely handles it:
let city = userProfile?.address?.city;
console.log(city); // undefined
Enter fullscreen mode Exit fullscreen mode
  • Nullish Coalescing Operator (??): A logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined, and otherwise returns its left-hand side operand.
    • Why use ?? instead of ||? The Logical OR (||) operator falls back to the right side if the left side is any falsy value (like 0, "", false, null, undefined). This causes bugs when 0 or "" are actually valid, intended values (e.g., a video with 0 views). The ?? operator fixes this by only falling back if the value is explicitly missing (null or undefined).
// Scenario: A video has exactly 0 views.
let videoViews = 0; 
let defaultViews = 10;

// Bug with OR operator (||): 
// JavaScript sees 0 as "falsy" and thinks the value is missing, so it applies the default.
console.log(videoViews || defaultViews); // Output: 10 (Incorrect: it overwrote our valid 0 views!)

// Fix with Nullish Coalescing (??):
// JavaScript checks if videoViews is exactly `null` or `undefined`. Since it's a valid 0, it keeps it.
console.log(videoViews ?? defaultViews); // Output: 0 (Correct!)

// If the value was actually missing:
let missingViews = null;
console.log(missingViews ?? defaultViews); // Output: 10 (Correctly applies default)
Enter fullscreen mode Exit fullscreen mode

Backtick for Strings with values

  • Backticks () are used to create Template Literals, allowing for string interpolation (embedding variables/expressions inside strings).
  • Example with backtick strings with values:
let author = "Preyum Kumar";
let topic = "NLP";
let message = `${author} is an expert in ${topic}.`;
console.log(message);
Enter fullscreen mode Exit fullscreen mode
  • Also if we want $ sign also to be printed use escape character for the same:
let price = 500;
let costMessage = `The total cost is \$${price}.`;
console.log(costMessage); // "The total cost is $500."
Enter fullscreen mode Exit fullscreen mode

Getters and Setters

  • Getters (get) bind an object property to a function that will be called when that property is looked up.
  • Setters (set) bind an object property to a function to be called when there is an attempt to set that property.
let person = {
    firstName: "Preyum",
    lastName: "Kumar",

    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    },

    set updateFirstName(newName) {
        if(newName.length > 0) {
            this.firstName = newName;
        } else {
            console.log("Name cannot be empty");
        }
    }
};

console.log(person.fullName); // "Preyum Kumar"
person.updateFirstName = "PreyumK";
console.log(person.fullName); // "PreyumK Kumar"
Enter fullscreen mode Exit fullscreen mode

Asynchronous JavaScript

  • JavaScript is single-threaded and synchronous by default. Asynchronous JavaScript allows the code to continue running other tasks while waiting for long-running operations (like fetching data, reading files) to complete, preventing the browser or server from freezing.
console.log("1. Start");

// This mimics a long-running task
setTimeout(() => {
    console.log("2. Async Operation Finished");
}, 1000);

console.log("3. End");
// Output order will be: 1, 3, 2
Enter fullscreen mode Exit fullscreen mode

The setTimeout() Function

  • setTimeout() is a method that calls a function or evaluates an expression after a specified number of milliseconds.
console.log("Waiting for my Apple...");
setTimeout(() => {
    console.log("Apple is served!");
}, 2000); // Delays execution by 2000 milliseconds (2 seconds)
Enter fullscreen mode Exit fullscreen mode

Callbacks

  • A callback is a function passed as an argument to another function, which is then invoked inside the outer function to complete some kind of routine or action.
  • Example of order and production (without order, production cannot start):
let order = (call_production) => {
    console.log("Order placed, please call production.");
    // We execute the callback function here
    call_production();
};

let production = () => {
    console.log("Order received, starting production of Apple Juice.");
};

order(production); 
Enter fullscreen mode Exit fullscreen mode
  • Issues with callbacks, the callback hell: Callback hell (or Pyramid of Doom) happens when multiple asynchronous operations are chained together using nested callbacks, making the code hard to read and maintain. Promises (discussed next) solve this.
// Callback Hell Example
step1(function() {
    step2(function() {
        step3(function() {
            step4(function() {
                console.log("All steps finished!");
            });
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

Promises

  • A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It solves the callback hell problem by allowing chaining.
  • Change example to use promises:
function makeOrder() {
    return new Promise((resolve, reject) => {
        let isStockAvailable = true;

        if (isStockAvailable) {
            resolve("Order placed successfully"); // This goes to the first .then()
        } else {
            reject("Error: Out of stock"); // This skips directly to .catch()
        }
    });
}

makeOrder()
    // No semicolons between then, catch, finally so the code can cascade
    .then((message) => {
        // Step 1: Receives the resolved value from makeOrder
        console.log(message); // Output: "Order placed successfully"
        return "Starting production"; // This returned value is passed to the NEXT .then()
    })
    .then((nextStep) => {
        // Step 2: Receives the returned value from the previous .then()
        console.log(nextStep); // Output: "Starting production"
    })
    .catch((error) => {
        // Step 3: Only runs if reject() was called, or if an error was thrown above
        console.log(error); 
    })
    .finally(() => {
        // Step 4: Always runs at the very end, whether it was resolved or rejected
        console.log("Operation closed (runs regardless of success or failure).");
    });
Enter fullscreen mode Exit fullscreen mode

Async and Await

  • async and await are syntactic sugar on top of Promises, making asynchronous code look and behave more like synchronous code, which is much easier to read.
  • An async function always returns a Promise. The await keyword pauses the execution of the async function until the Promise settles (resolves or rejects).
function fetchFruit() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Mango");
        }, 1500);
    });
}

async function prepareSmoothie() {
    console.log("2. Starting prep inside async function...");

    try {
        // Execution inside this function pauses here until fetchFruit is done.
        // However, the rest of the synchronous code outside this function keeps running!
        let fruit = await fetchFruit(); 
        console.log(`4. Smoothie made with ${fruit}!`);
    } catch (error) {
        console.log("Failed to get fruit.");
    }
}

console.log("1. Ordering Smoothie...");

// Calling the async function. It starts running synchronously until it hits 'await'.
prepareSmoothie();

console.log("3. Doing other tasks while waiting for the smoothie...");

/* 
Output Order:
1. Ordering Smoothie...
2. Starting prep inside async function...
3. Doing other tasks while waiting for the smoothie...
(After 1.5 seconds delay)
4. Smoothie made with Mango!
*/
Enter fullscreen mode Exit fullscreen mode

Modules (Import and Export)

  • Modules allow you to break your code into separate files. This makes it easier to maintain the code base. React and Next.js are entirely built around importing and exporting components and functions.
  • Named Exports: You can create named exports two ways: individually or all at once at the bottom. You can have multiple named exports per file. When importing, you must use the exact same name inside curly braces {}. You can also rename (alias) imports using the as keyword to avoid naming conflicts.
// mathUtils.js (Exporting)

// 1. Exporting individually on the same line
export const add = (a, b) => a + b;

// 2. Declaring first, exporting at the bottom
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;

export { subtract, multiply }; 

// app.js (Importing)
// Using 'as' to alias 'add' to 'addNumbers'
import { add as addNumbers, subtract, multiply } from './mathUtils.js';

console.log(addNumbers(5, 3)); // 8
console.log(multiply(2, 4)); // 8
Enter fullscreen mode Exit fullscreen mode
  • Default Exports: You can only have one default export per file. It is often used for exporting the main component in a file. When importing, you can name it anything you want and do not use curly braces.
// Greeting.js (Exporting)
const Greeting = () => "Hello, Preyum!";
export default Greeting;

// app.js (Importing)
// We can name it whatever we want, let's call it GreetComp
import GreetComp from './Greeting.js';
console.log(GreetComp()); // "Hello, Preyum!"
Enter fullscreen mode Exit fullscreen mode

Stay tuned for my next blog, where I'll dive into React! In the meantime, if you feel I missed anything here, drop a comment below and I'll make sure to add it.

Top comments (0)