diff --git a/.env b/.env index 594b3ac..78e2fee 100644 --- a/.env +++ b/.env @@ -1 +1,5 @@ -VITE_SERVER_URL=http://localhost:3000 \ No newline at end of file +STRAVA_EXPIRATION_TIME=1683606397 +STRAVA_CACHED_REFRESH_TOKEN=25bd1cf38e37ee07e53d446f10ef812daee4d9bc +STRAVA_CACHED_TOKEN=25b0ab3f4831c7fba452bfa39910b730db955be7 +STRAVA_CLIENT_ID=105494 +STRAVA_CLIENT_SECRET=aeca4b4832d195c467d23e1c465781eed04be76b \ No newline at end of file diff --git a/.gitignore b/.gitignore index 12ac647..680989a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.DS_Store \ No newline at end of file +.DS_Store +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..da387d5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/src/test.js" + } + ] +} \ No newline at end of file diff --git a/assets/staticGoodreadsFeed.json b/assets/staticGoodreadsFeed.json index 181e61f..543ca64 100644 --- a/assets/staticGoodreadsFeed.json +++ b/assets/staticGoodreadsFeed.json @@ -1 +1 @@ -{"title":"Spencer's Updates","description":"Recent updates from Spencer","link":"https://www.goodreads.com/user/show/104822881-spencer-attick","image":"https://www.goodreads.com/images/layout/goodreads_logo_144.jpg","category":[],"items":[{"id":"ReadStatus6500933037","title":"Spencer wants to read 'The New Jim Crow: Mass Incarceration in the Age of Colorblindness'","description":"\n \"The\n Spencer wants to read The New Jim Crow: Mass Incarceration in the Age of Colorblindness\n by\n Michelle Alexander\n
\n ","link":"https://www.goodreads.com/review/show/4352519893","published":1681150264000,"created":1681150264000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6500925868","title":"Spencer wants to read 'Under a White Sky: The Nature of the Future'","description":"\n \"Under\n Spencer wants to read Under a White Sky: The Nature of the Future\n by\n Elizabeth Kolbert\n
\n ","link":"https://www.goodreads.com/review/show/4839952039","published":1681150116000,"created":1681150116000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6500641760","title":"Spencer wants to read 'How We Get Free: Black Feminism and the Combahee River Collective'","description":"\n \"How\n Spencer wants to read How We Get Free: Black Feminism and the Combahee River Collective\n by\n Keeanga-Yamahtta Taylor\n
\n ","link":"https://www.goodreads.com/review/show/5476252974","published":1681144425000,"created":1681144425000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6491139783","title":"Spencer wants to read 'Show Your Work!: 10 Ways to Share Your Creativity and Get Discovered'","description":"\n \"Show\n Spencer wants to read Show Your Work!: 10 Ways to Share Your Creativity and Get Discovered\n by\n Austin Kleon\n
\n ","link":"https://www.goodreads.com/review/show/5469503948","published":1680887722000,"created":1680887722000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6478030577","title":"Spencer wants to read 'Walking in the Woods: Go back to nature with the Japanese way of shinrin-yoku'","description":"\n \"Walking\n Spencer wants to read Walking in the Woods: Go back to nature with the Japanese way of shinrin-yoku\n by\n Yoshifumi Miyazaki\n
\n ","link":"https://www.goodreads.com/review/show/5460083875","published":1680533426000,"created":1680533426000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6470808816","title":"Spencer wants to read 'The Seven Husbands of Evelyn Hugo'","description":"\n \"The\n Spencer wants to read The Seven Husbands of Evelyn Hugo\n by\n Taylor Jenkins Reid\n
\n ","link":"https://www.goodreads.com/review/show/5454861866","published":1680347678000,"created":1680347678000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6469804206","title":"Spencer started reading 'The Satanic Verses'","description":"\n \"The\n Spencer started reading The Satanic Verses\n by\n Salman Rushdie\n
\n ","link":"https://www.goodreads.com/review/show/5441536242","published":1680312945000,"created":1680312945000,"category":[],"enclosures":[],"media":{}},{"id":"Review5410621036","title":"Spencer added 'Read Dangerously: The Subversive Power of Literature in Troubled Times'","description":"\n \"Read\n Spencer gave 5 stars to Read Dangerously: The Subversive Power of Literature in Troubled Times (Hardcover)\n by\n Azar Nafisi\n
\n \n\n \n ","link":"https://www.goodreads.com/review/show/5410621036","published":1680312945000,"created":1680312945000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6469801096","title":"Spencer has read 'The Phantom Tollbooth'","description":"\n \"The\n Spencer has read The Phantom Tollbooth\n by\n Norton Juster\n
\n ","link":"https://www.goodreads.com/review/show/5454123961","published":1680312866000,"created":1680312866000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus6456841873","title":"Spencer wants to read 'Gate of the Sun'","description":"\n \"Gate\n Spencer wants to read Gate of the Sun\n by\n Elias Khoury\n
\n ","link":"https://www.goodreads.com/review/show/5444784188","published":1679953095000,"created":1679953095000,"category":[],"enclosures":[],"media":{}}]} \ No newline at end of file +{"title":"Spencer's Updates","description":"Recent updates from Spencer","link":"https://www.goodreads.com/user/show/104822881-spencer-attick","image":"https://www.goodreads.com/images/layout/goodreads_logo_144.jpg","category":[],"items":[{"id":"Review6409737317","title":"Spencer added 'Freedom is a Constant Struggle'","description":"\n \"Freedom\n Spencer gave 5 stars to Freedom is a Constant Struggle (Paperback)\n by\n Angela Y. Davis\n
\n \n\n \n ","link":"https://www.goodreads.com/review/show/6409737317","published":1712511806000,"created":1712511806000,"category":[],"enclosures":[],"media":{}},{"id":"Review5836026781","title":"Spencer added 'An Autobiography'","description":"\n \"An\n Spencer gave 5 stars to An Autobiography (Paperback)\n by\n Angela Y. Davis\n
\n \n\n \n ","link":"https://www.goodreads.com/review/show/5836026781","published":1712450674000,"created":1712450674000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7789676355","title":"Spencer wants to read 'My Grandmother: A Memoir'","description":"\n \"My\n Spencer wants to read My Grandmother: A Memoir\n by\n Fethiye Çetin\n
\n ","link":"https://www.goodreads.com/review/show/6407410317","published":1712435671000,"created":1712435671000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7789505447","title":"Spencer wants to read 'Claudette Colvin: Twice Toward Justice'","description":"\n \"Claudette\n Spencer wants to read Claudette Colvin: Twice Toward Justice\n by\n Phillip Hoose\n
\n ","link":"https://www.goodreads.com/review/show/6407289935","published":1712432484000,"created":1712432484000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7789503043","title":"Spencer wants to read 'The Montgomery Bus Boycott and the Women Who Started It: The Memoir of Jo Ann Gibson Robinson'","description":"\n \"The\n Spencer wants to read The Montgomery Bus Boycott and the Women Who Started It: The Memoir of Jo Ann Gibson Robinson\n by\n Jo Ann Gibson Robinson\n
\n ","link":"https://www.goodreads.com/review/show/6407288181","published":1712432435000,"created":1712432435000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7788986390","title":"Spencer wants to read 'Normal Life by Dean Spade'","description":"\n \"Normal\n Spencer wants to read Normal Life by Dean Spade\n by\n Dean Spade\n
\n ","link":"https://www.goodreads.com/review/show/6406918073","published":1712422109000,"created":1712422109000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7788984321","title":"Spencer wants to read 'Queer (In)Justice: The Criminalization of LGBT People in the United States'","description":"\n \"Queer\n Spencer wants to read Queer (In)Justice: The Criminalization of LGBT People in the United States\n by\n Joey L. Mogul\n
\n ","link":"https://www.goodreads.com/review/show/6406916566","published":1712422066000,"created":1712422066000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7788982934","title":"Spencer wants to read 'Captive Genders: Trans Embodiment and the Prison Industrial Complex'","description":"\n \"Captive\n Spencer wants to read Captive Genders: Trans Embodiment and the Prison Industrial Complex\n by\n Eric A. Stanley\n
\n ","link":"https://www.goodreads.com/review/show/6406915533","published":1712422037000,"created":1712422037000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7775479776","title":"Spencer wants to read 'Lilith'","description":"\n \"Lilith\n Spencer wants to read Lilith\n by\n Eric Rickstad\n
\n ","link":"https://www.goodreads.com/review/show/6397247001","published":1712103187000,"created":1712103187000,"category":[],"enclosures":[],"media":{}},{"id":"ReadStatus7774671812","title":"Spencer has read 'Yellowface'","description":"\n \"Yellowface\n Spencer has read Yellowface\n by\n R.F. Kuang\n
\n ","link":"https://www.goodreads.com/review/show/6396669101","published":1712089261000,"created":1712089261000,"category":[],"enclosures":[],"media":{}}]} \ No newline at end of file diff --git a/assets/staticMediumFeed.json b/assets/staticMediumFeed.json index a057512..c3507b8 100644 --- a/assets/staticMediumFeed.json +++ b/assets/staticMediumFeed.json @@ -1 +1 @@ -{"title":"Stories by Spencer Attick on Medium","description":"Stories by Spencer Attick on Medium","link":"https://medium.com/@spencer.attick?source=rss-e5dc359f27c2------2","image":"https://cdn-images-1.medium.com/fit/c/150/150/1*-uzWbooKuJ4W-gL2TBuWkA.png","category":[],"items":[{"id":"https://medium.com/p/17f6e29a07ba","title":"A Guide to Immediately Invoked Function Expressions (IIFE)","link":"https://medium.com/@spencer.attick/a-guide-to-immediately-invoked-function-expressions-iife-17f6e29a07ba?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1681325789000,"created":1681325789000,"category":["privacy","javascript","security","code","software-development"],"content":"
\"\"

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log(\"Hello, I am a self-invoking arrow function!\");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

\"\"

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = 'secret';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log(\"Returning cached data\");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log(\"Fetching data from server\");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

\"\"","enclosures":[],"content_encoded":"
\"\"

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log(\"Hello, I am a self-invoking arrow function!\");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

\"\"

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = 'secret';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log(\"Returning cached data\");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log(\"Fetching data from server\");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

\"\"","media":{}},{"id":"https://medium.com/p/c3214c897b4c","title":"Project: Script to Automatically Update Resources in my Portfolio","link":"https://medium.com/@spencer.attick/project-script-to-automatically-update-resources-in-my-portfolio-c3214c897b4c?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1681220946000,"created":1681220946000,"category":["portfolio","nodejs","software-development","javascript","script"],"content":"
\"\"

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

\"\"","enclosures":[],"content_encoded":"
\"\"

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

\"\"","media":{}},{"id":"https://medium.com/p/a4c2e35b2558","title":"Project: Creating Code Examples for Segment’s Server-Side Libraries","link":"https://medium.com/@spencer.attick/project-creating-code-examples-for-all-of-segments-server-side-libraries-a4c2e35b2558?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1680791081000,"created":1680791081000,"category":["python","segment","servers","ruby","nodejs"],"content":"
\"\"

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

\"\"

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

\"\"

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

\"\"

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

\"\"","enclosures":[],"content_encoded":"
\"\"

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

\"\"

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

\"\"

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

\"\"

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

\"\"","media":{}},{"id":"https://medium.com/p/fc36c10c02e5","title":"Project: Story Points","link":"https://medium.com/@spencer.attick/project-story-points-fc36c10c02e5?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1680618193000,"created":1680618193000,"category":["javascript","support","optimization","zendesk","story-points"],"content":"
\"\"

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

\"\"

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

\"\"

From there, logic is in place to add points up based on which fields are used:

\"\"

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

\"\"

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

\"\"

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

\"\"","enclosures":[],"content_encoded":"
\"\"

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

\"\"

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

\"\"

From there, logic is in place to add points up based on which fields are used:

\"\"

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

\"\"

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

\"\"

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

\"\"","media":{}},{"id":"https://medium.com/p/f5efe0afd046","title":"Project: Updating Segment’s analytics-node Library to Include TypeScript","link":"https://medium.com/@spencer.attick/project-updating-segments-analytics-node-library-to-include-typescript-f5efe0afd046?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1678993785000,"created":1678993785000,"category":["segment","codecademy","node","typescript"],"content":"
\"\"

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

\"\"","enclosures":[],"content_encoded":"
\"\"

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

\"\"","media":{}},{"id":"https://medium.com/p/9251d1438121","title":"Project: Segment Source Function — Auth0","link":"https://medium.com/@spencer.attick/project-segment-source-function-auth0-9251d1438121?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1678737033000,"created":1678737033000,"category":["auth0","javascript","node","segment"],"content":"

Project: Segment Source Function — Auth0

\"\"

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

\"\"","enclosures":[],"content_encoded":"

Project: Segment Source Function — Auth0

\"\"

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

\"\"","media":{}},{"id":"https://medium.com/p/7f03b1b9e1b9","title":"Scope, Hoisting, and Closure in Javascript","link":"https://medium.com/@spencer.attick/scope-and-hoisting-and-closure-in-javascript-7f03b1b9e1b9?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1675365832000,"created":1675365832000,"category":["javascript-tips","hoisting","javascript","scopes","closure"],"content":"
\"\"

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

\"\"

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

\"\"

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

\"\"","enclosures":[],"content_encoded":"
\"\"

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

\"\"

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

\"\"

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

\"\"","media":{}},{"id":"https://medium.com/p/a8add44f764d","title":"Memory & Automatic Memory Management (Garbage Collection) in Javascript","link":"https://medium.com/@spencer.attick/memory-automatic-memory-management-garbage-collection-in-javascript-a8add44f764d?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1674931467000,"created":1674931467000,"category":["javascript","chrome","v8-engine","scavenging","mark-and-sweep"],"content":"
\"\"

What is memory allocation? As a programmer working for years with languages like Javascript and Python, memory usage and management hasn’t been something I’ve had to think much about and, quite frankly, have taken for granted. Not knowing what it is or how it works has left me out of the loop in terms of the inner workings of how a few very important principles work in Javascript specifically.

So let’s take a look!

What is memory management?

All software programs use memory in some form or another. When we’re talking about memory we’re referring to physical space that is needed to define and run files on a computer. These days, technology is improving more and more to the point where the physical components that store and process memory are smaller than every before which means that even something as the size of your phone is extremely powerful in terms of what it can do with the physical memory allocation it has (especially compared to the room-sized computers of the past— check out a visual history of computers here). To control what is using space on a device, memory management is used.

Memory management refers to the process by which variable values and other references are removed from memory when a program no longer uses them. For example:

let testVar = 'Some string';
testVar = null;

On the first line, we’ve declared a variable testVar and have given it the value 'Some string’ . That value, 'Some string', needs to live somewhere in a computer’s memory so it can be referenced later. However, on the second line, we’re setting testVar to null which removes the reference of testVar to 'Some string' making 'Some string' a piece of data that now lives in memory without a useable reference to it. Remember, memory usage on a computer denotes a usage of physical space so we can’t just leave 'Some string' floating around in memory indefinitely. If we did, all of those unreferenced pieces of data would eventually take up all of the available space a computer has for memory and then your computer would become useless (rather needlessly).

Some programming languages require the engineer to manage the memory needed to store data in things like variables manually. These languages are closer to machine code in nature and are known as low-level languages, such as C++. Others, know as high-level languages like Javascript, Python, and Ruby, have much of the complexity of managing memory on a program abstracted away by making those processes automatic.

How is memory stored in Javascript?

Javascript has two mechanisms for storing memory: the stack and the heap.

The Stack

Primitive data types (string, number, boolean, etc.) have their values stored in the stack along with their keys or references.

For example the following would be stored in a stack:

\"\"

A stack allocates a constant amount of data per item which means primitive types have a limit in terms of how large they can become which is known as static memory allocation.

The Heap

We’ve seen that the stack will hold references to data and the data itself for primitive data types. What about objects, functions, and arrays?

We call these reference data types in terms of memory management. This is because the stack will hold references to objects, functions, and arrays but will not store those values itself. Reference data types have their values stored in a heap.

Here we can see that the reference types listed in the stack groceryList (an array) and menu (an object), have a reference in the stack but their values in the heap:

\"\"

The heap can do what’s called dynamic memory allocation to adjust for the space that a reference data type is using. The only limit to the amount of space a reference data type can use is the amount of total memory available for usage.

Why does this matter for day-to-day Javascript programming?

As a Javascript developer, you need to keep this storage system in mind as it impacts how your variables are stored and updated.

For example, take a look at this code:

\"\"

Notice that setting newMenu to menu and then updating newMenu actually updates both newMenu and menu. That is because when you assign newMenu to menu, they’re then both pointing to the same exact object in the heap.

Be careful with this though. Consider the following example:

\"\"

We have the same situation here as with the first example. The newMenu variable points at menu. So why, when the console.logs() are run, do both variables not resolve to an empty object since newMenu was set to {} on line 9? Aren’t both menu and newMenu pointing to the same spot in memory?

They were! But on line 9 when newMenu was set equal to {}, it’s actually being reassigned to a new object. Now menu is still pointing at the original object but, by using the = assignment operator, newMenu is now pointing at a new empty object so the variables are no longer connected.

This works the same way with arrays and functions:

\"\"
\"\"

Now that we know a little more about what memory in Javascript looks like, what happens when we have a reference that’s no longer used?

Garbage Collection

Automatic Memory Management (known more colloquially as garbage collection) is the system by which Javascript engines decide which references no longer exist and remove them. This happens out-of-the-box and in such a way that engineers usually don’t need to be too concerned with it their day to day work.

Javascript’s V8 engine (which powers Chrome and Node.js) uses a method called generational garbage collection which involves processes called scavenging and mark-and-sweep.

Generational Garbage Collection

The V8 engine keeps track of new and old data (referred to as “generations”) by taking brand new data and running it through a process called scavenging. It will run its scavenging mechanism several times on the same data (new generation data) such that data that continues to be referenced will be kept and data that loses its references will be removed from memory. Data that remains after several scavenging cycles will be moved out of the space that new data had been occupying and will be stored in a longer term manner where garbage collection, called mark-and-sweep at this next phase, will happen more infrequently. This data is now considered old generation data.

Scavenging

Brand new data coming into memory is scavenged several times before memory graduates to become old generation in memory. During the process of scavenging, data with existing references are copied to a new space. The previous space where this data existed is cleared of data that no longer has references and that space is then ready to ingest data that has yet to be seen (even newer data) into memory. This scavenging process will happen to data a few times before data is moved considered “old generation”. Scavenging is a fast method of garbage collection that happens with new data that is most likely to lose its reference quickly. Once data graduates to old generation it is then no longer scavenged but is subject to the more infrequent process of mark-and-sweep instead.

Mark-and-Sweep

With mark-and-sweep, the Javascript garbage collector mechanism will traverse all data in the old generation of memory and will “mark” any data that has an existing reference with which that data is reachable by programs being run on a machine. Then, in its second phase of operation, any data that has not been marked will be cleared away and removed from memory.

Resources

https://www.youtube.com/watch?v=Hci9Bb4_fkA (great video about memory)

https://www.youtube.com/watch?v=AeUCN2lPqL8 (pretty good — longer and a little less straightforward)

https://www.youtube.com/watch?v=DIzouoy13UM

https://felixgerschau.com/javascript-memory-management/ (very good)

https://www.youtube.com/watch?v=FZkCh_GeftY (good examples/explanation of reference-counting and mark and sweep)

https://www.geeksforgeeks.org/memory-management-in-javascript/

\"\"","enclosures":[],"content_encoded":"
\"\"

What is memory allocation? As a programmer working for years with languages like Javascript and Python, memory usage and management hasn’t been something I’ve had to think much about and, quite frankly, have taken for granted. Not knowing what it is or how it works has left me out of the loop in terms of the inner workings of how a few very important principles work in Javascript specifically.

So let’s take a look!

What is memory management?

All software programs use memory in some form or another. When we’re talking about memory we’re referring to physical space that is needed to define and run files on a computer. These days, technology is improving more and more to the point where the physical components that store and process memory are smaller than every before which means that even something as the size of your phone is extremely powerful in terms of what it can do with the physical memory allocation it has (especially compared to the room-sized computers of the past— check out a visual history of computers here). To control what is using space on a device, memory management is used.

Memory management refers to the process by which variable values and other references are removed from memory when a program no longer uses them. For example:

let testVar = 'Some string';
testVar = null;

On the first line, we’ve declared a variable testVar and have given it the value 'Some string’ . That value, 'Some string', needs to live somewhere in a computer’s memory so it can be referenced later. However, on the second line, we’re setting testVar to null which removes the reference of testVar to 'Some string' making 'Some string' a piece of data that now lives in memory without a useable reference to it. Remember, memory usage on a computer denotes a usage of physical space so we can’t just leave 'Some string' floating around in memory indefinitely. If we did, all of those unreferenced pieces of data would eventually take up all of the available space a computer has for memory and then your computer would become useless (rather needlessly).

Some programming languages require the engineer to manage the memory needed to store data in things like variables manually. These languages are closer to machine code in nature and are known as low-level languages, such as C++. Others, know as high-level languages like Javascript, Python, and Ruby, have much of the complexity of managing memory on a program abstracted away by making those processes automatic.

How is memory stored in Javascript?

Javascript has two mechanisms for storing memory: the stack and the heap.

The Stack

Primitive data types (string, number, boolean, etc.) have their values stored in the stack along with their keys or references.

For example the following would be stored in a stack:

\"\"

A stack allocates a constant amount of data per item which means primitive types have a limit in terms of how large they can become which is known as static memory allocation.

The Heap

We’ve seen that the stack will hold references to data and the data itself for primitive data types. What about objects, functions, and arrays?

We call these reference data types in terms of memory management. This is because the stack will hold references to objects, functions, and arrays but will not store those values itself. Reference data types have their values stored in a heap.

Here we can see that the reference types listed in the stack groceryList (an array) and menu (an object), have a reference in the stack but their values in the heap:

\"\"

The heap can do what’s called dynamic memory allocation to adjust for the space that a reference data type is using. The only limit to the amount of space a reference data type can use is the amount of total memory available for usage.

Why does this matter for day-to-day Javascript programming?

As a Javascript developer, you need to keep this storage system in mind as it impacts how your variables are stored and updated.

For example, take a look at this code:

\"\"

Notice that setting newMenu to menu and then updating newMenu actually updates both newMenu and menu. That is because when you assign newMenu to menu, they’re then both pointing to the same exact object in the heap.

Be careful with this though. Consider the following example:

\"\"

We have the same situation here as with the first example. The newMenu variable points at menu. So why, when the console.logs() are run, do both variables not resolve to an empty object since newMenu was set to {} on line 9? Aren’t both menu and newMenu pointing to the same spot in memory?

They were! But on line 9 when newMenu was set equal to {}, it’s actually being reassigned to a new object. Now menu is still pointing at the original object but, by using the = assignment operator, newMenu is now pointing at a new empty object so the variables are no longer connected.

This works the same way with arrays and functions:

\"\"
\"\"

Now that we know a little more about what memory in Javascript looks like, what happens when we have a reference that’s no longer used?

Garbage Collection

Automatic Memory Management (known more colloquially as garbage collection) is the system by which Javascript engines decide which references no longer exist and remove them. This happens out-of-the-box and in such a way that engineers usually don’t need to be too concerned with it their day to day work.

Javascript’s V8 engine (which powers Chrome and Node.js) uses a method called generational garbage collection which involves processes called scavenging and mark-and-sweep.

Generational Garbage Collection

The V8 engine keeps track of new and old data (referred to as “generations”) by taking brand new data and running it through a process called scavenging. It will run its scavenging mechanism several times on the same data (new generation data) such that data that continues to be referenced will be kept and data that loses its references will be removed from memory. Data that remains after several scavenging cycles will be moved out of the space that new data had been occupying and will be stored in a longer term manner where garbage collection, called mark-and-sweep at this next phase, will happen more infrequently. This data is now considered old generation data.

Scavenging

Brand new data coming into memory is scavenged several times before memory graduates to become old generation in memory. During the process of scavenging, data with existing references are copied to a new space. The previous space where this data existed is cleared of data that no longer has references and that space is then ready to ingest data that has yet to be seen (even newer data) into memory. This scavenging process will happen to data a few times before data is moved considered “old generation”. Scavenging is a fast method of garbage collection that happens with new data that is most likely to lose its reference quickly. Once data graduates to old generation it is then no longer scavenged but is subject to the more infrequent process of mark-and-sweep instead.

Mark-and-Sweep

With mark-and-sweep, the Javascript garbage collector mechanism will traverse all data in the old generation of memory and will “mark” any data that has an existing reference with which that data is reachable by programs being run on a machine. Then, in its second phase of operation, any data that has not been marked will be cleared away and removed from memory.

Resources

https://www.youtube.com/watch?v=Hci9Bb4_fkA (great video about memory)

https://www.youtube.com/watch?v=AeUCN2lPqL8 (pretty good — longer and a little less straightforward)

https://www.youtube.com/watch?v=DIzouoy13UM

https://felixgerschau.com/javascript-memory-management/ (very good)

https://www.youtube.com/watch?v=FZkCh_GeftY (good examples/explanation of reference-counting and mark and sweep)

https://www.geeksforgeeks.org/memory-management-in-javascript/

\"\"","media":{}},{"id":"https://medium.com/p/5f5bfabd3a89","title":"A First Look at the Javascript Event Loop on the Browser","link":"https://medium.com/@spencer.attick/a-first-look-at-the-javascript-event-loop-on-the-browser-5f5bfabd3a89?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1674060961000,"created":1674060961000,"category":["programming","javascript","javascript-event-loop","code","event-loop"],"content":"
\"\"

When you execute Javascript on the client, there are some behind-the-scenes processes that make the magic happen. As Javascript is a single-threaded language (it can only do one thing at a time), it needs a way to be told what aspects of your codebase should be run immediately, which should wait on things like API responses and timeouts, and in what order all of that should happen.

Enter, the Event Loop!

The Event Loop takes care of managing the execution of your program in a way that accounts for asynchronous behavior, the nesting of functions, and more.

What does the Event Loop look like?

At a high level, we can think of the Event Loop like this:

\"\"

Before we step through any code, let’s align on each of these pieces.

The Code Input will simply be the Javascript file you intend to run.

The Call Stack refers to a mechanism that processes each element in your code. It is a stack data structure which is known as first-in-last-out. That means that the first item that is pushed into the Call Stack (as with any stack) will be processed last. It’s like if you went to the library and searched the selves for books you wanted to take home. If you placed each book you found on top of the last, you’d have a pile of books with the first book you took off of the selves on the bottom and the most recent book you found on top. If you handed that pile to the librarian, they would likely take the top book from the pile to check it out for you first. Operating in that manner, they would check out the FIRST book you found on the shelves LAST (after checking all the other books out for you).

The Outside API/Javascript API section refers to an act of using one of the API methods available in the browser (like setTimeout) or the use of another outside API. When requests are made out to APIs, those actions are generally considered asynchronous as they deviate from Javascript running and completing tasks line by line, in order (synchronously).

The Event Queue is a queue data structure similar to a stack, but rather than being first-in-last-out, a queue is first-in-first-out. In terms of a queue, you can think of waiting in a line at the grocery store. If you enter the line first, the cashier will help you to check out before everyone else in the line. If you enter the line last, you’ll have to wait for each customer in front of you to check out, in order, before you’ll be allowed to check out. The first person in the line is the first person to have their groceries scanned and to leave. All programming queues work in this way, including the Event Queue.

The Event Loop is a mechanism that keeps track of whether or not there are any operations needing to be processed in the Event Queue. If there are items that need handling in the queue, it will loop through them and add them to the Call Stack one at a time. The Event Loop can only add items to the Call Stack if the Call Stack is totally empty.

Finally, the Code Output will help us visualize how the Code Input is run in terms of letting us see the order of operations. Consider this depiction to be something like running a file from your browser to see what prints in the console out and when.

How does the Event Loop work?

Now that we have a sense of the different parts of this process and a high-level understanding of each, let’s look at an example to help us understand how a file is read in Javascript and how each line is fed through the Event Loop.

To begin, the first line is a console.log() which is pushed into the Call Stack:

\"\"

There isn’t any process to wait on here in order for the console.log() to complete, so it’s immediately resolved from the stack and the result is displayed in the Code Output section of this diagram but you can imagine this would be printed to the console of the browser:

\"\"

With that, the first line is processed and First line to run! is printed out.

Next we’ll look at a setTimeout() function which actually makes use of a timer API available in the browser to keep track of when the milliseconds passed in as the second argument to that function have transpired:

\"\"

First, the setTimeout() is added to the Call Stack. If you’re unfamiliar, this function takes two arguments: a function to execute and a number to represent the amount of milliseconds to wait until the function passed in as the first argument is called.

The mechanism that keeps track of the time here is a built-in browser API. As that is the case, the next step will be to invoke that timer on the browser itself:

\"\"

With the timer set to run on the browser, the setTimeout() function is finished being processed in the Call Stack. Next we can look at the console.log() on line 7 while wait for the browser API to return the callback we passed to setTimeout() . I encourage you to think through what you expect to happen before continuing on:

\"\"

As with the previous console.log() , this one can be resolved immediately from the Call Stack and Last line to run! will be printed in the Code Output:

\"\"

Now, we can imagine the 1000 milliseconds have completed and the browser API is now ready to send the callback in to be processed. This looks like the API handing the callback to the Event Queue. Remember, Javascript can only do one thing at a time so if it was still resolving the console.log() on line 7 by the time 1000 milliseconds had gone by, we’d need to ensure any new activity didn’t interrupt any currently running processes. To that end, the callback is handed to the Event Queue to await a time when the Call Stack is completely cleared:

\"\"

The Event Loop will run when the Call Stack is completely clear to see what’s in the Event Queue:

\"\"

It sees the console.log() and pulls that into the empty Call Stack:

\"\"

Now the Call Stack has the capacity to process this console.log() just like the other two:

\"\"

From there, every line of code has been run and all aspects of the process are cleared. That’s it!

\"\"

Is that all there is to it?

This guide is meant to give you a high-level overview of what the Event Loop is and how it works. The process has a lot more nuance if you were to spend time digging into it. Once you feel you’ve grasped the basics here, go ahead and see what else you can find!

In this article we have looked at a case where the Call Stack only has one item in it at a time, there are many instances where the Call Stack would have more than one item in it such as instances where a function calls another function. Remember, the item on the top of the Call Stack is processed first even though that is the last item to be added. Feel free to try this out yourself here: Loupe.

What other resources are out there to help me further understand this concept?

There are TONS of great resources out there on the Event Loop. Here are a few that I referenced and found helpful:

Loupe — A tool that will help you visualize how your own code would run through the Event Loop.

JavaScript Event Loop Explained in 5 Minutes — A quick video that is fairly thorough.

What the heck is the event loop anyway? | Philip Roberts | JSConf EU — A longer, more in depth look at the Event Loop (the person giving this talk created the Loupe tool).

\"\"","enclosures":[],"content_encoded":"
\"\"

When you execute Javascript on the client, there are some behind-the-scenes processes that make the magic happen. As Javascript is a single-threaded language (it can only do one thing at a time), it needs a way to be told what aspects of your codebase should be run immediately, which should wait on things like API responses and timeouts, and in what order all of that should happen.

Enter, the Event Loop!

The Event Loop takes care of managing the execution of your program in a way that accounts for asynchronous behavior, the nesting of functions, and more.

What does the Event Loop look like?

At a high level, we can think of the Event Loop like this:

\"\"

Before we step through any code, let’s align on each of these pieces.

The Code Input will simply be the Javascript file you intend to run.

The Call Stack refers to a mechanism that processes each element in your code. It is a stack data structure which is known as first-in-last-out. That means that the first item that is pushed into the Call Stack (as with any stack) will be processed last. It’s like if you went to the library and searched the selves for books you wanted to take home. If you placed each book you found on top of the last, you’d have a pile of books with the first book you took off of the selves on the bottom and the most recent book you found on top. If you handed that pile to the librarian, they would likely take the top book from the pile to check it out for you first. Operating in that manner, they would check out the FIRST book you found on the shelves LAST (after checking all the other books out for you).

The Outside API/Javascript API section refers to an act of using one of the API methods available in the browser (like setTimeout) or the use of another outside API. When requests are made out to APIs, those actions are generally considered asynchronous as they deviate from Javascript running and completing tasks line by line, in order (synchronously).

The Event Queue is a queue data structure similar to a stack, but rather than being first-in-last-out, a queue is first-in-first-out. In terms of a queue, you can think of waiting in a line at the grocery store. If you enter the line first, the cashier will help you to check out before everyone else in the line. If you enter the line last, you’ll have to wait for each customer in front of you to check out, in order, before you’ll be allowed to check out. The first person in the line is the first person to have their groceries scanned and to leave. All programming queues work in this way, including the Event Queue.

The Event Loop is a mechanism that keeps track of whether or not there are any operations needing to be processed in the Event Queue. If there are items that need handling in the queue, it will loop through them and add them to the Call Stack one at a time. The Event Loop can only add items to the Call Stack if the Call Stack is totally empty.

Finally, the Code Output will help us visualize how the Code Input is run in terms of letting us see the order of operations. Consider this depiction to be something like running a file from your browser to see what prints in the console out and when.

How does the Event Loop work?

Now that we have a sense of the different parts of this process and a high-level understanding of each, let’s look at an example to help us understand how a file is read in Javascript and how each line is fed through the Event Loop.

To begin, the first line is a console.log() which is pushed into the Call Stack:

\"\"

There isn’t any process to wait on here in order for the console.log() to complete, so it’s immediately resolved from the stack and the result is displayed in the Code Output section of this diagram but you can imagine this would be printed to the console of the browser:

\"\"

With that, the first line is processed and First line to run! is printed out.

Next we’ll look at a setTimeout() function which actually makes use of a timer API available in the browser to keep track of when the milliseconds passed in as the second argument to that function have transpired:

\"\"

First, the setTimeout() is added to the Call Stack. If you’re unfamiliar, this function takes two arguments: a function to execute and a number to represent the amount of milliseconds to wait until the function passed in as the first argument is called.

The mechanism that keeps track of the time here is a built-in browser API. As that is the case, the next step will be to invoke that timer on the browser itself:

\"\"

With the timer set to run on the browser, the setTimeout() function is finished being processed in the Call Stack. Next we can look at the console.log() on line 7 while wait for the browser API to return the callback we passed to setTimeout() . I encourage you to think through what you expect to happen before continuing on:

\"\"

As with the previous console.log() , this one can be resolved immediately from the Call Stack and Last line to run! will be printed in the Code Output:

\"\"

Now, we can imagine the 1000 milliseconds have completed and the browser API is now ready to send the callback in to be processed. This looks like the API handing the callback to the Event Queue. Remember, Javascript can only do one thing at a time so if it was still resolving the console.log() on line 7 by the time 1000 milliseconds had gone by, we’d need to ensure any new activity didn’t interrupt any currently running processes. To that end, the callback is handed to the Event Queue to await a time when the Call Stack is completely cleared:

\"\"

The Event Loop will run when the Call Stack is completely clear to see what’s in the Event Queue:

\"\"

It sees the console.log() and pulls that into the empty Call Stack:

\"\"

Now the Call Stack has the capacity to process this console.log() just like the other two:

\"\"

From there, every line of code has been run and all aspects of the process are cleared. That’s it!

\"\"

Is that all there is to it?

This guide is meant to give you a high-level overview of what the Event Loop is and how it works. The process has a lot more nuance if you were to spend time digging into it. Once you feel you’ve grasped the basics here, go ahead and see what else you can find!

In this article we have looked at a case where the Call Stack only has one item in it at a time, there are many instances where the Call Stack would have more than one item in it such as instances where a function calls another function. Remember, the item on the top of the Call Stack is processed first even though that is the last item to be added. Feel free to try this out yourself here: Loupe.

What other resources are out there to help me further understand this concept?

There are TONS of great resources out there on the Event Loop. Here are a few that I referenced and found helpful:

Loupe — A tool that will help you visualize how your own code would run through the Event Loop.

JavaScript Event Loop Explained in 5 Minutes — A quick video that is fairly thorough.

What the heck is the event loop anyway? | Philip Roberts | JSConf EU — A longer, more in depth look at the Event Loop (the person giving this talk created the Loupe tool).

\"\"","media":{}},{"id":"https://medium.com/p/2d07585c38a5","title":"A Beginners Guide to Async/Await in Javascript","link":"https://medium.com/@spencer.attick/a-beginners-guide-to-async-await-in-javascript-2d07585c38a5?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1673310470000,"created":1673310470000,"category":["asynchronous","coding","code","javascript","computer-science"],"content":"
\"\"
Photo courtesy of Unsplash

What is Async/Await?

Async/await is a concept that arose in the 2017 version of Javascript that allows for asynchronous actions (where one or more things are happening at a time in a program) to be accounted for synchronously (in order). In Javascript, asynchronous behavior generally arises in the form of API requests or manually instantiated Promises. Most other functionality in Javascript is synchronous.

The usage of async/await extends the Promise feature was released into Javascript in 2015. Promises are a solution to nesting callback functions which was how asynchronous behavior was handled before Promises were released. Callbacks (a function passed to another function as an argument) created a problem, often referred to as “callback hell” where functions would be nested to the point of illegibility. Promises allowed for the chaining of functions with the .then() method in order to introduce more readability into a program. Async/await takes things a step further to move away from nesting and chaining into a syntax that looks much more like the synchronous Javascript most folks are used to.

Async/await was developed as “syntactic sugar” on top of Promises and can only be used with a Promise that has been previously created or to handle HTTP responses (which is usually a Promise under the hood).

The async keyword labels a function as containing asynchronous content (a Promise resolution or an HTTP request). The await keyword, indicates which line the program should pause on to wait for the resolution of a Promise.

Here is a simple example:

const promiseExample = new Promise((resolve, reject) => {
const randomBoolean = Math.random() < 0.5;
if (randomBoolean) {
console.log(randomBoolean);
resolve('Boolean was true!')
} else {
console.log(randomBoolean);
reject('Boolean was false!')
}
})

async function asyncExample() {
try {
const promiseOutcome = await promiseExample;
console.log('SUCCESS:', promiseOutcome);
} catch(err) {
console.log('ERROR:', err);
}
}

asyncExample();

Here we have a variable, promiseExample, which takes the result of a Promise. In this case, the randomBoolean variable will randomly resolve to either true or false . If it is true, then the Promise is coded to resolve successfully. If it’s false, then the Promise will reject, or fail.

The asyncExample function is labeled with the async keyword which indicates that it contains asynchronous content. From there, the await keyword tells the program which line to wait on until it receives a value (or an error is thrown).

If an asynchronous action takes places without properly being handled, then you may find that variables that are dependent on the outcome of that asynchronous action are undefined. That happens when the program attempts to read and use those variables before they have been assigned a value by the Promise structure. For that reason, it’s important to properly handle Promises with callbacks, a .then() chain, or by using async/await .

Go ahead and run the above code a few times to see what you get!

When to Use Async/Await

The async/await syntax is ONLY used in conjunction with Promises. It is an optimization that was designed specifically to help make the asynchronous actions that occur when Promises are involved much more readable. Async/await allows you to read code as it will execute, line by line, rather than having to jump around in your code to see what will happen next in sequence.

Advantages of using Async/Await

Async/await allows you to make your code more readable. It moves away from nesting callbacks or chaining .then() methods, both of which can make your code messy and difficult to debug.

The improvement that comes with async/await allows you to run asynchronous code line by line just as you do with synchronous code.

Error Handling

Error handling with async/await look a bit different than if you’re only using the Promise structure. Remember, with Promises, you’ll handle errors with .catch() like this:

doSomething()
.then(() => )
.then(() => )
.catch((err) => )

With async/await , errors are handled with a try/catch block like this:

async function doSomething() {
try {

} catch(err) {

}
}

This syntax will attempt to run your code in the try portion of the block and if ANY of the Promises referenced there with await fail, then they will be caught with the catch section of the block.

Don’t forget the try/catch block! If you do, and your Promise rejects then your code will throw an Unhandled Promise error as you haven’t given the error message an outlet.

Example

Now that we have a decent amount of context, let’s consider a quick example. First we can create a function that will generate a random number:

function generateRandomNum() {
const randomNum = Math.floor(Math.random() * 11);
if (randomNum === 4) {
return 'FOUR';
} else {
return randomNum;
}
}

You may notice that this function contains a twist to keep things interesting. It will return a random number UNLESS that number ends up being 4 in which case it will return a string, FOUR .

Next, we can create a function that returns the result of a Promise which checks the datatype of the number generated by the generateRandomNum() function. This function takes one argument, operandPosition, which will help us narrow in on whether the first or second operand (or number to be added together) is the result of a problem:

function generateNumPromise(operandPosition) {
const randomNum = generateRandomNum();
let result = new Promise((resolve, reject) => {
if (typeof randomNum === 'number') {
resolve(randomNum);
} else {
reject(`There was a problem! The data type generated for the ${operandPosition} operand was not a number.`);
}
})
console.log(`The ${operandPosition} number is ${randomNum}.`)
return result;
}

This function contains a Promise which will resolve (be considered successful) if randomNum is a number datatype or will reject (be considered erroneous) if randomNum is a string. Either way, a console.log will print out what each operand is each time the program is run for visibility. The result of the Promise will then be returned as result .

From there, we’ll use an async function to await the resolution of each Promise generated by calling generateNumPromise() twice. Remember, async/await is only used when there is a Promise to wait for, so you’ll never need it unless you’ve added a Promise to your code or you’re making an HTTP request as those generally return Promises:

async function addNumbers() {
try {
const firstNum = await generateNumPromise('first');
const secondNum = await generateNumPromise('second');
console.log(`The sum of firstNum and secondNum = ${firstNum + secondNum}.`);
} catch (err) {
console.error(err);
}
}

addNumbers();

Here is are a couple runs of the program:

\"\"
\"\"

The first demonstrates a case where both operands involved are numbers which allows the program to complete without error.

In the second screenshot, we can see that a string was generated instead of a number which does throw an error, invoking the reject message in the try/catch block.

Key Takeaways

Additional Resources

Synchronous vs. Asynchronous in Javascript

Javascript Promises vs Async Await EXPLAINED (in 5 minutes) video

\"\"","enclosures":[],"content_encoded":"
\"\"
Photo courtesy of Unsplash

What is Async/Await?

Async/await is a concept that arose in the 2017 version of Javascript that allows for asynchronous actions (where one or more things are happening at a time in a program) to be accounted for synchronously (in order). In Javascript, asynchronous behavior generally arises in the form of API requests or manually instantiated Promises. Most other functionality in Javascript is synchronous.

The usage of async/await extends the Promise feature was released into Javascript in 2015. Promises are a solution to nesting callback functions which was how asynchronous behavior was handled before Promises were released. Callbacks (a function passed to another function as an argument) created a problem, often referred to as “callback hell” where functions would be nested to the point of illegibility. Promises allowed for the chaining of functions with the .then() method in order to introduce more readability into a program. Async/await takes things a step further to move away from nesting and chaining into a syntax that looks much more like the synchronous Javascript most folks are used to.

Async/await was developed as “syntactic sugar” on top of Promises and can only be used with a Promise that has been previously created or to handle HTTP responses (which is usually a Promise under the hood).

The async keyword labels a function as containing asynchronous content (a Promise resolution or an HTTP request). The await keyword, indicates which line the program should pause on to wait for the resolution of a Promise.

Here is a simple example:

const promiseExample = new Promise((resolve, reject) => {
const randomBoolean = Math.random() < 0.5;
if (randomBoolean) {
console.log(randomBoolean);
resolve('Boolean was true!')
} else {
console.log(randomBoolean);
reject('Boolean was false!')
}
})

async function asyncExample() {
try {
const promiseOutcome = await promiseExample;
console.log('SUCCESS:', promiseOutcome);
} catch(err) {
console.log('ERROR:', err);
}
}

asyncExample();

Here we have a variable, promiseExample, which takes the result of a Promise. In this case, the randomBoolean variable will randomly resolve to either true or false . If it is true, then the Promise is coded to resolve successfully. If it’s false, then the Promise will reject, or fail.

The asyncExample function is labeled with the async keyword which indicates that it contains asynchronous content. From there, the await keyword tells the program which line to wait on until it receives a value (or an error is thrown).

If an asynchronous action takes places without properly being handled, then you may find that variables that are dependent on the outcome of that asynchronous action are undefined. That happens when the program attempts to read and use those variables before they have been assigned a value by the Promise structure. For that reason, it’s important to properly handle Promises with callbacks, a .then() chain, or by using async/await .

Go ahead and run the above code a few times to see what you get!

When to Use Async/Await

The async/await syntax is ONLY used in conjunction with Promises. It is an optimization that was designed specifically to help make the asynchronous actions that occur when Promises are involved much more readable. Async/await allows you to read code as it will execute, line by line, rather than having to jump around in your code to see what will happen next in sequence.

Advantages of using Async/Await

Async/await allows you to make your code more readable. It moves away from nesting callbacks or chaining .then() methods, both of which can make your code messy and difficult to debug.

The improvement that comes with async/await allows you to run asynchronous code line by line just as you do with synchronous code.

Error Handling

Error handling with async/await look a bit different than if you’re only using the Promise structure. Remember, with Promises, you’ll handle errors with .catch() like this:

doSomething()
.then(() => )
.then(() => )
.catch((err) => )

With async/await , errors are handled with a try/catch block like this:

async function doSomething() {
try {

} catch(err) {

}
}

This syntax will attempt to run your code in the try portion of the block and if ANY of the Promises referenced there with await fail, then they will be caught with the catch section of the block.

Don’t forget the try/catch block! If you do, and your Promise rejects then your code will throw an Unhandled Promise error as you haven’t given the error message an outlet.

Example

Now that we have a decent amount of context, let’s consider a quick example. First we can create a function that will generate a random number:

function generateRandomNum() {
const randomNum = Math.floor(Math.random() * 11);
if (randomNum === 4) {
return 'FOUR';
} else {
return randomNum;
}
}

You may notice that this function contains a twist to keep things interesting. It will return a random number UNLESS that number ends up being 4 in which case it will return a string, FOUR .

Next, we can create a function that returns the result of a Promise which checks the datatype of the number generated by the generateRandomNum() function. This function takes one argument, operandPosition, which will help us narrow in on whether the first or second operand (or number to be added together) is the result of a problem:

function generateNumPromise(operandPosition) {
const randomNum = generateRandomNum();
let result = new Promise((resolve, reject) => {
if (typeof randomNum === 'number') {
resolve(randomNum);
} else {
reject(`There was a problem! The data type generated for the ${operandPosition} operand was not a number.`);
}
})
console.log(`The ${operandPosition} number is ${randomNum}.`)
return result;
}

This function contains a Promise which will resolve (be considered successful) if randomNum is a number datatype or will reject (be considered erroneous) if randomNum is a string. Either way, a console.log will print out what each operand is each time the program is run for visibility. The result of the Promise will then be returned as result .

From there, we’ll use an async function to await the resolution of each Promise generated by calling generateNumPromise() twice. Remember, async/await is only used when there is a Promise to wait for, so you’ll never need it unless you’ve added a Promise to your code or you’re making an HTTP request as those generally return Promises:

async function addNumbers() {
try {
const firstNum = await generateNumPromise('first');
const secondNum = await generateNumPromise('second');
console.log(`The sum of firstNum and secondNum = ${firstNum + secondNum}.`);
} catch (err) {
console.error(err);
}
}

addNumbers();

Here is are a couple runs of the program:

\"\"
\"\"

The first demonstrates a case where both operands involved are numbers which allows the program to complete without error.

In the second screenshot, we can see that a string was generated instead of a number which does throw an error, invoking the reject message in the try/catch block.

Key Takeaways

Additional Resources

Synchronous vs. Asynchronous in Javascript

Javascript Promises vs Async Await EXPLAINED (in 5 minutes) video

\"\"","media":{}}]} \ No newline at end of file +{"title":"Stories by Spencer Attick on Medium","description":"Stories by Spencer Attick on Medium","link":"https://medium.com/@spencer.attick?source=rss-e5dc359f27c2------2","image":"https://cdn-images-1.medium.com/fit/c/150/150/1*_8WJXYOSojpzAxZ8hddk4g.jpeg","category":[],"items":[{"id":"https://medium.com/p/0793f2ee2f7e","title":"Project: Learning Python by Coding Simple TicTacToe Game","link":"https://medium.com/@spencer.attick/project-learning-python-by-coding-simple-tictactoe-game-0793f2ee2f7e?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1710348622000,"created":1710348622000,"category":["software-development","study","coding","javascript","python"],"content":"
\"\"

As a trained JavaScript developer working for years in the language, I’d been wanting to branch out into the functional possibilities of other languages. I took Codecademy’s Computer Science path which is taught in Python and was eager to step into a new challenge. As a part of my learning, I built a simple command line Tic Tac Toe game to solidify some Python syntax and concepts.

The Problem

To this point, I’ve gained extensive knowledge and time spent working with JavaScript and was ready to up my skill game by learning Python.

The Work

Codecademy’s Computer Science track offers a great intro to Python at the start of the course. I learned through their project sets to come to a working knowledge of the Python language.

The code base for this project consists of just two files: one containing a Board class that holds the methods and variables needed to run the game and the other is a brief file for executing the logic of the program.

The Code

You can find the code here: https://github.com/spencerattick/tic_tac_toe.

What I Learned

This project was a great experience! I had actually worked on something similar in JavaScript but through a bit of up front planning, managed to code this Python version much more succinctly. This work gave me a great opporunity to code outside of Codecademy’s guardrails to stand something up on my own. It was a wonderful way to solidify and further practice the new Python syntax I had learned.

\"\"","enclosures":[],"content_encoded":"
\"\"

As a trained JavaScript developer working for years in the language, I’d been wanting to branch out into the functional possibilities of other languages. I took Codecademy’s Computer Science path which is taught in Python and was eager to step into a new challenge. As a part of my learning, I built a simple command line Tic Tac Toe game to solidify some Python syntax and concepts.

The Problem

To this point, I’ve gained extensive knowledge and time spent working with JavaScript and was ready to up my skill game by learning Python.

The Work

Codecademy’s Computer Science track offers a great intro to Python at the start of the course. I learned through their project sets to come to a working knowledge of the Python language.

The code base for this project consists of just two files: one containing a Board class that holds the methods and variables needed to run the game and the other is a brief file for executing the logic of the program.

The Code

You can find the code here: https://github.com/spencerattick/tic_tac_toe.

What I Learned

This project was a great experience! I had actually worked on something similar in JavaScript but through a bit of up front planning, managed to code this Python version much more succinctly. This work gave me a great opporunity to code outside of Codecademy’s guardrails to stand something up on my own. It was a wonderful way to solidify and further practice the new Python syntax I had learned.

\"\"","media":{}},{"id":"https://medium.com/p/40244b17df2c","title":"Integrating with the Strava API","link":"https://levelup.gitconnected.com/integrating-with-the-strava-api-40244b17df2c?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1682603854000,"created":1682603854000,"category":["javascript","code","strava","technology","api"],"content":"
\"\"
Alessio Soggetti for Unsplash

As I was coding my personal website I was looking for ways that I could personalize the project. I didn’t want it to just be a place to house blog posts and project walkthroughs. I wanted to share a bit of myself there as well.

With that in mind, I integrated the Goodreads RSS feed as books are one of my passions. While I was working on my portfolio I was also training for my first half-marathon and doing a TON of running that I was recording on Strava. I thought to myself, why not add that data onto my website as well?

I saw that they have an API that uses OAuth2, with which I didn’t have much experience. After a bit of trial and error along with a variety of internet resources I was able to get things up and running.

To save you some time, I thought I’d write a simple guide to the process. Strava does provide information here but I’ll pare things down for you even more step by step. Here is Strava’s documentation if you’d like the full rundown.

Let’s get started!

1. Get App Credentials from Strava
The first step you’ll need to take is to head over to Strava to register an app (it’s way easier than it sounds) and then get your access keys. The URL to do this can be hard to find so here it is for your reference: https://www.strava.com/settings/api.

Once you’re there, you’ll need to fill out a few fields in order to be provided with your credentials. The fields filled out here are not incredibly important.

\"\"
Image from Strava’s documentation

The field that tripped me up here was the Authorization Callback Domain. This is only used once for the next step so you can set it to localhost to get things working. You don’t need to have a localhost running or take any additional steps to get the Authorization Callback Domain to work, just fill in that form with localhostas the Authorization Callback Domain and that’s it.

2. Authorize Credentials in the Browser
This step only happens once and it involves visiting a specific URL in the browser so that you can authorize the use of the credentials Strava gave you in the previous step.

My goal in using the API was to use the List Athlete Activities endpoint which requires read_all access. You’ll need to do this step for any level of access you’d like, but the following URL will be for read_all access specifically.

What you’ll want to do here is paste this URL into your browser: // http://localhost/exchange_token?state=&code=1c49f418910a609b0097ae5ce0016c9b8141e8cd&scope=read,activity:read_all. Go ahead and provide the URL with the client_id from the your Strava account.

\"\"

3. Get Strava Access Code
Awesome! When you visit the above URL you’ll be prompted to on the website to authorize your project with Strava:

\"\"

Once you click Authorize, you’ll be redirected to a new URL that looks like this:http://localhost/exchange_token?state=&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&scope=read,activity:read_all.

You won’t see anything render on that page and that’s just fine. All you need there is the new URL.

I’ve redacted some the code value I received there as a query parameter but that is the field you’ll want to hold on to for the next step.

4. Get Strava Access and Refresh Tokens
After you’ve collected the code value, you’ll want to make an API request (you can use Postman, cURL, or any other API request tool or server to do this) to get the actual access token information.

The request will be a POST to this endpoint:

https://www.strava.com/oauth/token

You’ll want to provide the following as query parameters:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to authorization_code

The complete URL will look like this (I’ve redacted my own values):

https://www.strava.com/oauth/token?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&grant_type=authorization_code

If you’re using Postman, here is what the interface should look like:

\"\"

Making that request will result in a response that looks like this:

{
\"token_type\": \"Bearer\",
\"expires_at\": 1681350948,
\"expires_in\": 21600,
\"refresh_token\": \"25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",
\"access_token\": \"87b0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",
\"athlete\": {
\"id\": xxxxxx,
\"username\": \"xxxxxxx\",
\"resource_state\": 2,
\"firstname\": \"Spencer\",
\"lastname\": \"Attick\",
\"bio\": null,
\"city\": \"Oakland\",
\"state\": \"California\",
\"country\": \"United States\",
\"sex\": null,
\"premium\": false,
\"summit\": false,
\"created_at\": \"2015-04-01T00:46:54Z\",
\"updated_at\": \"2023-04-11T18:24:23Z\",
\"badge_type_id\": 0,
\"weight\": 0.0,
\"profile_medium\": \"https://graph.facebook.com/1226490129/picture?height=256&width=256\",
\"profile\": \"https://graph.facebook.com/1226490129/picture?height=256&width=256\",
\"friend\": null,
\"follower\": null
}
}

The important fields to pay attention to here are expires_at, refresh_token, and access_token.

Strava’s access_token will expire at the expires_at time which is a Unix Epoch timestamp. We’ll talk about refreshing later, but for now let’s get some Strava data.

5. Get Workout Data from Strava
Alright! We can finally make a request to get the data we want from Strava!

Here you’ll want to make a GET request that looks like this:

curl --location 'https://www.strava.com/api/v3/athlete/activities' \\
--header 'Authorization: Bearer '

You’ll use the access_token you received above as the Bearer Token here. Ensure that you don’t have a request body at all for this step as that will throw an error.

Check out the response to that request. You’ve got Strava data you can work with! You can add this to your personal website, create a workout dashboard for yourself, create a leaderboard with friends, or whatever else you can think of!

7. Manage Refreshing the Access Token
The last thing we want to take care of is making sure you can refresh your token when it expires. To do this, you’ll want to keep track of the last expires_at field you received. When the current time is greater than the value of the expires_at value, it’s time for a new access_token.

You’ll make this POST request to get a new token:

curl --location --request POST 'https://www.strava.com/oauth/token

Here are the query params you’ll use and their values:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to refresh_token
refresh_token: the refresh_token from your last successful authentication request

Here is the full request with the query params in place:

?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&grant_type=refresh_token&refresh_token=25bd1cf38exxxxxxxxxxxxxxxxxxxxx'

The response will look like this:

{
\"token_type\": \"Bearer\",
\"access_token\": \"6e6e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",
\"expires_at\": 1681430228,
\"expires_in\": 21600,
\"refresh_token\": \"25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"
}

Now you’re all set! You can make requests to Strava to get meaningful data on your activities and you can generate a new access token!

From here, you’ll want to put everything together into something you can use in your server. Here is the code I use for Strava integration that I wrote for my personal website:

const isStravaTokenExpired = (currentExpirationTime) => {
const currentEpochTime = Date.now();
if (currentExpirationTime === 'undefined') {
return `There is an error with the currentEpirationTime, it's value is: ${currentExpirationTime}.`
}
return currentEpochTime > currentExpirationTime;
}

const generateNewToken = async () => {
console.log('Generating new token...');
const requestOptions = {
method: 'POST',
redirect: 'follow'
};
const requestURL = `https://www.strava.com/oauth/token?client_id=${process.env.STRAVA_CLIENT_ID}&client_secret=${process.env.STRAVA_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=${process.env.STRAVA_CACHED_REFRESH_TOKEN}`;

try {
let response = await fetch(requestURL, requestOptions);
response = await response.json();
if (response.message === 'Bad Request') {
console.log(response);
return;
}
return {
refreshToken: await response.refresh_token,
expirationTime: await response.expires_at,
accessToken: await response.access_token
}
} catch (error) {
console.log(error);
}

}



const persistNewTokenData = async (newTokenData) => {
// Read the .env file
const envBuffer = fs.readFileSync('.env');
const envConfig = dotenv.parse(envBuffer);

// Update the relevant key with the new value
envConfig['STRAVA_EXPIRATION_TIME'] = newTokenData.expirationTime,
envConfig['STRAVA_CACHED_REFRESH_TOKEN'] = newTokenData.refreshToken,
envConfig['STRAVA_CACHED_TOKEN'] = newTokenData.accessToken

// Write the updated key-value pair to the file
const envText = Object.keys(envConfig).map(key => `${key}=${envConfig[key]}`).join('\\n');
await fs.promises.writeFile('.env', envText);
};



const getStravaActivityData = async () => {
console.log('Requesting activity data from Strava...');
let myHeaders = new Headers();
myHeaders.append(\"Authorization\", `Bearer ${process.env.STRAVA_CACHED_TOKEN}`);

const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};

try {
const response = await fetch(\"https://www.strava.com/api/v3/athlete/activities\", requestOptions);
return response.json();

} catch (error) {
console.log('error', error)
}
}

//check to see if the expiration time has passed
const executeStravaLogic = async () => {
const isTokenExpired = isStravaTokenExpired(process.env.STRAVA_EXPIRATION_TIME);

if (typeof isTokenExpired === 'string') {
console.log('Please resolve the error with the current expiration time stored in the .env file.');
return;
} else if (isTokenExpired) {
console.log('The expiration time has passed. Generating a new token...');
//if yes - generate a new token
const newTokenData = await generateNewToken();
if (!newTokenData.expirationTime) {
console.log('There was an error getting refresh token data.')
return;
}
//save the new token info to .env
persistNewTokenData(newTokenData);

}
//make request to Strava activities endpoint
const stravaActivityData = await getStravaActivityData();
return stravaActivityData;
};

app.get('/api/strava', executeStravaLogic);

Feel free to use what works for you and scrap what doesn’t.

Enjoy!

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

\"\"

Integrating with the Strava API was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

","enclosures":[],"content_encoded":"
\"\"
Alessio Soggetti for Unsplash

As I was coding my personal website I was looking for ways that I could personalize the project. I didn’t want it to just be a place to house blog posts and project walkthroughs. I wanted to share a bit of myself there as well.

With that in mind, I integrated the Goodreads RSS feed as books are one of my passions. While I was working on my portfolio I was also training for my first half-marathon and doing a TON of running that I was recording on Strava. I thought to myself, why not add that data onto my website as well?

I saw that they have an API that uses OAuth2, with which I didn’t have much experience. After a bit of trial and error along with a variety of internet resources I was able to get things up and running.

To save you some time, I thought I’d write a simple guide to the process. Strava does provide information here but I’ll pare things down for you even more step by step. Here is Strava’s documentation if you’d like the full rundown.

Let’s get started!

1. Get App Credentials from Strava
The first step you’ll need to take is to head over to Strava to register an app (it’s way easier than it sounds) and then get your access keys. The URL to do this can be hard to find so here it is for your reference: https://www.strava.com/settings/api.

Once you’re there, you’ll need to fill out a few fields in order to be provided with your credentials. The fields filled out here are not incredibly important.

\"\"
Image from Strava’s documentation

The field that tripped me up here was the Authorization Callback Domain. This is only used once for the next step so you can set it to localhost to get things working. You don’t need to have a localhost running or take any additional steps to get the Authorization Callback Domain to work, just fill in that form with localhostas the Authorization Callback Domain and that’s it.

2. Authorize Credentials in the Browser
This step only happens once and it involves visiting a specific URL in the browser so that you can authorize the use of the credentials Strava gave you in the previous step.

My goal in using the API was to use the List Athlete Activities endpoint which requires read_all access. You’ll need to do this step for any level of access you’d like, but the following URL will be for read_all access specifically.

What you’ll want to do here is paste this URL into your browser: // http://localhost/exchange_token?state=&code=1c49f418910a609b0097ae5ce0016c9b8141e8cd&scope=read,activity:read_all. Go ahead and provide the URL with the client_id from the your Strava account.

\"\"

3. Get Strava Access Code
Awesome! When you visit the above URL you’ll be prompted to on the website to authorize your project with Strava:

\"\"

Once you click Authorize, you’ll be redirected to a new URL that looks like this:http://localhost/exchange_token?state=&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&scope=read,activity:read_all.

You won’t see anything render on that page and that’s just fine. All you need there is the new URL.

I’ve redacted some the code value I received there as a query parameter but that is the field you’ll want to hold on to for the next step.

4. Get Strava Access and Refresh Tokens
After you’ve collected the code value, you’ll want to make an API request (you can use Postman, cURL, or any other API request tool or server to do this) to get the actual access token information.

The request will be a POST to this endpoint:

https://www.strava.com/oauth/token

You’ll want to provide the following as query parameters:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to authorization_code

The complete URL will look like this (I’ve redacted my own values):

https://www.strava.com/oauth/token?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&grant_type=authorization_code

If you’re using Postman, here is what the interface should look like:

\"\"

Making that request will result in a response that looks like this:

{
\"token_type\": \"Bearer\",
\"expires_at\": 1681350948,
\"expires_in\": 21600,
\"refresh_token\": \"25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",
\"access_token\": \"87b0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",
\"athlete\": {
\"id\": xxxxxx,
\"username\": \"xxxxxxx\",
\"resource_state\": 2,
\"firstname\": \"Spencer\",
\"lastname\": \"Attick\",
\"bio\": null,
\"city\": \"Oakland\",
\"state\": \"California\",
\"country\": \"United States\",
\"sex\": null,
\"premium\": false,
\"summit\": false,
\"created_at\": \"2015-04-01T00:46:54Z\",
\"updated_at\": \"2023-04-11T18:24:23Z\",
\"badge_type_id\": 0,
\"weight\": 0.0,
\"profile_medium\": \"https://graph.facebook.com/1226490129/picture?height=256&width=256\",
\"profile\": \"https://graph.facebook.com/1226490129/picture?height=256&width=256\",
\"friend\": null,
\"follower\": null
}
}

The important fields to pay attention to here are expires_at, refresh_token, and access_token.

Strava’s access_token will expire at the expires_at time which is a Unix Epoch timestamp. We’ll talk about refreshing later, but for now let’s get some Strava data.

5. Get Workout Data from Strava
Alright! We can finally make a request to get the data we want from Strava!

Here you’ll want to make a GET request that looks like this:

curl --location 'https://www.strava.com/api/v3/athlete/activities' \\
--header 'Authorization: Bearer '

You’ll use the access_token you received above as the Bearer Token here. Ensure that you don’t have a request body at all for this step as that will throw an error.

Check out the response to that request. You’ve got Strava data you can work with! You can add this to your personal website, create a workout dashboard for yourself, create a leaderboard with friends, or whatever else you can think of!

7. Manage Refreshing the Access Token
The last thing we want to take care of is making sure you can refresh your token when it expires. To do this, you’ll want to keep track of the last expires_at field you received. When the current time is greater than the value of the expires_at value, it’s time for a new access_token.

You’ll make this POST request to get a new token:

curl --location --request POST 'https://www.strava.com/oauth/token

Here are the query params you’ll use and their values:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to refresh_token
refresh_token: the refresh_token from your last successful authentication request

Here is the full request with the query params in place:

?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&grant_type=refresh_token&refresh_token=25bd1cf38exxxxxxxxxxxxxxxxxxxxx'

The response will look like this:

{
\"token_type\": \"Bearer\",
\"access_token\": \"6e6e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",
\"expires_at\": 1681430228,
\"expires_in\": 21600,
\"refresh_token\": \"25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"
}

Now you’re all set! You can make requests to Strava to get meaningful data on your activities and you can generate a new access token!

From here, you’ll want to put everything together into something you can use in your server. Here is the code I use for Strava integration that I wrote for my personal website:

const isStravaTokenExpired = (currentExpirationTime) => {
const currentEpochTime = Date.now();
if (currentExpirationTime === 'undefined') {
return `There is an error with the currentEpirationTime, it's value is: ${currentExpirationTime}.`
}
return currentEpochTime > currentExpirationTime;
}

const generateNewToken = async () => {
console.log('Generating new token...');
const requestOptions = {
method: 'POST',
redirect: 'follow'
};
const requestURL = `https://www.strava.com/oauth/token?client_id=${process.env.STRAVA_CLIENT_ID}&client_secret=${process.env.STRAVA_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=${process.env.STRAVA_CACHED_REFRESH_TOKEN}`;

try {
let response = await fetch(requestURL, requestOptions);
response = await response.json();
if (response.message === 'Bad Request') {
console.log(response);
return;
}
return {
refreshToken: await response.refresh_token,
expirationTime: await response.expires_at,
accessToken: await response.access_token
}
} catch (error) {
console.log(error);
}

}



const persistNewTokenData = async (newTokenData) => {
// Read the .env file
const envBuffer = fs.readFileSync('.env');
const envConfig = dotenv.parse(envBuffer);

// Update the relevant key with the new value
envConfig['STRAVA_EXPIRATION_TIME'] = newTokenData.expirationTime,
envConfig['STRAVA_CACHED_REFRESH_TOKEN'] = newTokenData.refreshToken,
envConfig['STRAVA_CACHED_TOKEN'] = newTokenData.accessToken

// Write the updated key-value pair to the file
const envText = Object.keys(envConfig).map(key => `${key}=${envConfig[key]}`).join('\\n');
await fs.promises.writeFile('.env', envText);
};



const getStravaActivityData = async () => {
console.log('Requesting activity data from Strava...');
let myHeaders = new Headers();
myHeaders.append(\"Authorization\", `Bearer ${process.env.STRAVA_CACHED_TOKEN}`);

const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};

try {
const response = await fetch(\"https://www.strava.com/api/v3/athlete/activities\", requestOptions);
return response.json();

} catch (error) {
console.log('error', error)
}
}

//check to see if the expiration time has passed
const executeStravaLogic = async () => {
const isTokenExpired = isStravaTokenExpired(process.env.STRAVA_EXPIRATION_TIME);

if (typeof isTokenExpired === 'string') {
console.log('Please resolve the error with the current expiration time stored in the .env file.');
return;
} else if (isTokenExpired) {
console.log('The expiration time has passed. Generating a new token...');
//if yes - generate a new token
const newTokenData = await generateNewToken();
if (!newTokenData.expirationTime) {
console.log('There was an error getting refresh token data.')
return;
}
//save the new token info to .env
persistNewTokenData(newTokenData);

}
//make request to Strava activities endpoint
const stravaActivityData = await getStravaActivityData();
return stravaActivityData;
};

app.get('/api/strava', executeStravaLogic);

Feel free to use what works for you and scrap what doesn’t.

Enjoy!

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

\"\"

Integrating with the Strava API was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

","media":{}},{"id":"https://medium.com/p/9778ad618cb4","title":"A Brief Introduction to Object Oriented Programming","link":"https://levelup.gitconnected.com/a-brief-introduction-to-object-oriented-programming-9778ad618cb4?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1682427096000,"created":1682427096000,"category":["javascript","object-oriented","technology","software-development"],"content":"
\"\"

What is it?

Object oriented programming (OOP) is a commonly used programming model that focuses on the use of classes and objects to create a codebase that is simple and reusable. It centers around the following concepts:

What is it used for?

By focusing on creating reusable pieces, OOP limits code duplication, makes code easier to read, and allows for the separation of concerns.

Due to its benefits and flexibility OOP can be used in nearly any type of project.

As it is so common and useful a framework, it’s generally something prospective employers will look for knowledge of either in a take home challenge or as a direct interview question.

What does it look like?

Without object oriented programming, you might write a piece of code in Javascript that looks like this:

\"\"

The above example simply instantiates variables for different aspects of a dog and the declares a function to make use of one of those variables.

There are a few problems with this in that the variables are globally scoped leaving them open to collision or accidental manipulation, the pieces of code are not connected in any meaningful way — they’re all just hanging out in the global scope, and the code isn’t reusable — it has specific values for the declared variables which makes this pretty useless outside of describing a single dog.

We can make a big change to all of that without writing too much additional code.

Here is a refactor towards object oriented programming:

\"\"

If we focus on the image to the left, we can see that the previous code which was loosely defined is now housed together in a single structure. This makes the code more readable in that we can easily tell that it’s all meant to be related.

The variables and function are now scoped to the class that was created which ensures less opportunity for collision or for them to be accidentally changed.

Additionally, the class we’ve created is now reusable. It isn’t limited to a single dog but is now broad enough to be able to accommodate any animal we’d like to supply. We can see this in action in the image on the right where dog and cat constants are declared and the class function is being made use of for each.

Much better, right?

Class Syntax in Javascript

I know you’re dying to get to the principles of object oriented programming, and we will get there, but first let’s take a closer look at the syntax for creating a class in Javascript.

\"\"

Here we have a Bike class (it’s conventional to use camel casing for class names with the first letter also capitalized).

We initialize with the class keyword, the name of the class (in this case, Bike), and then curly braces {} .

From there, we create a constructor which holds the variables with which you can instantiate new instances of your class. In this case, those variables are make, model, and color. For example, you can create new instances of Bike using different values:

const firstBike = new Bike('Trek', 'Madone', 'Red');

const secondBike = new Bike('Bianchi', 'Oltre', 'Black');

After the constructor, functions can be declared. Note that the syntax here doesn’t use the function keyword. You can just go ahead and give your function a name and you’re rolling.

The Four Principles Of OOP

Ok, now we know what object oriented programming looks like and why it’s useful. Let’s move onto some OOP terminology that you can throw out in an interview to get the job and that you should keep in mind as you’re writing clean, reusable code.

1. Encapsulation
We’ve discussed this concept already, so let’s put a name to it. Encapsulation refers to the grouping of related functions and properties together to connect concepts and create DRY (do not repeat yourself) code.

Remember when we looked at the block of code where everything was declared in the global scope? Encapsulation fixes the problems that can bring by housing related code in one data structure (be that a class or an object):

\"\"

2. Abstraction
We haven’t discussed this much yet but the concept of abstraction means to hide the complexity of your code or not allow the alteration of certain fields. You can think of this like any mechanical device (ex. a radio, microwave, TV, etc.). When you want to turn any of those things on, there is a mechanism to do so (ex. push a button). The actual mechanical functions that are performed to turn the appliance on within the device are not known to most of us and, frankly, we don’t much care as long as the thing turns on. The user has those processes obscured by the simplicity of the button push. That concept can also be applied to object oriented programming as abstraction.

Abstraction can be achieved in code through getters and setters which allow a user to only access or update certain fields in structured ways. We won’t talk much about how those work specifically here but managing how a user can see and make changes to the fields in your code makes a big difference in safeguarding the functionality of what you’ve written.

\"\"

In the above example, we can see that the Shape class on the left has no access methods which encourages direct manipulation of the constructor. In contrast, the example on the right has specific methods that are written to get and set the number of sides a shape might have. The functionality is a bit simplistic for the sake of explanation there, but you can also add in checks to ensure things like the number of sides provided is actually a number and not another datatype.

3. Inheritance
Inheritance simply refers to child classes having the attributes of their parent classes passed down to them. This allows a programmer to write less code since methods that have already been written for a parent class don’t need to be rewritten for a child class to have access to them.

\"\"

In the example above, a Person class is declared on the left with a method called introduceYourself() included on it. We can see on the right that a Student class is created which extends the Person class. This means that all of the functionality that the Person class has, the Student class also now has which we can see in action with the sayNameAndGrade() method on the Student class which makes use of the Person class’s introduceYourself() method without redefining it.

4. Polymorphism
Within the concept of polymorphism, the functionality of a child/parent chain of classes is determined at runtime. An example of polymorphism could be the method of a child class overriding the same method on a parent class. As the name suggests, through polymorphism object functionality can take “many forms”. The interaction between parent and children classes can be as simple as the child class inheriting functionality from the parent class or it can be more complex with overlapping methods or child functionality overriding that of the parent. This complexity is called polymorphism and it gives a developer more options in terms of what choices to make with the many available tools a chain or family of classes provides.

\"\"

We can see a simple example above with the code on the left defining a Shape class. That class has a method on it called calcArea(). On the right, we see that Rectangle and Triangle classes extend the Shape class. These two child classes also each have a calcArea() method. When we create a Triangle instance and call calcArea() on it, it’s not the Shape's calcArea() function that is invoked, but the Triangle's. This is a simple example, but it does illustrate the flexibility inherent in working with classes that we call polymorphism.

Alright! That’s it! The last bit of wisdom I’ll leave you with is a way to remember object oriented programming terminology so you can always be ready to write better code or to ace an interview:

Abstraction

Polymorphism
Inheritance
Encapsulation

Additional Resources

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

\"\"

A Brief Introduction to Object Oriented Programming was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

","enclosures":[],"content_encoded":"
\"\"

What is it?

Object oriented programming (OOP) is a commonly used programming model that focuses on the use of classes and objects to create a codebase that is simple and reusable. It centers around the following concepts:

What is it used for?

By focusing on creating reusable pieces, OOP limits code duplication, makes code easier to read, and allows for the separation of concerns.

Due to its benefits and flexibility OOP can be used in nearly any type of project.

As it is so common and useful a framework, it’s generally something prospective employers will look for knowledge of either in a take home challenge or as a direct interview question.

What does it look like?

Without object oriented programming, you might write a piece of code in Javascript that looks like this:

\"\"

The above example simply instantiates variables for different aspects of a dog and the declares a function to make use of one of those variables.

There are a few problems with this in that the variables are globally scoped leaving them open to collision or accidental manipulation, the pieces of code are not connected in any meaningful way — they’re all just hanging out in the global scope, and the code isn’t reusable — it has specific values for the declared variables which makes this pretty useless outside of describing a single dog.

We can make a big change to all of that without writing too much additional code.

Here is a refactor towards object oriented programming:

\"\"

If we focus on the image to the left, we can see that the previous code which was loosely defined is now housed together in a single structure. This makes the code more readable in that we can easily tell that it’s all meant to be related.

The variables and function are now scoped to the class that was created which ensures less opportunity for collision or for them to be accidentally changed.

Additionally, the class we’ve created is now reusable. It isn’t limited to a single dog but is now broad enough to be able to accommodate any animal we’d like to supply. We can see this in action in the image on the right where dog and cat constants are declared and the class function is being made use of for each.

Much better, right?

Class Syntax in Javascript

I know you’re dying to get to the principles of object oriented programming, and we will get there, but first let’s take a closer look at the syntax for creating a class in Javascript.

\"\"

Here we have a Bike class (it’s conventional to use camel casing for class names with the first letter also capitalized).

We initialize with the class keyword, the name of the class (in this case, Bike), and then curly braces {} .

From there, we create a constructor which holds the variables with which you can instantiate new instances of your class. In this case, those variables are make, model, and color. For example, you can create new instances of Bike using different values:

const firstBike = new Bike('Trek', 'Madone', 'Red');

const secondBike = new Bike('Bianchi', 'Oltre', 'Black');

After the constructor, functions can be declared. Note that the syntax here doesn’t use the function keyword. You can just go ahead and give your function a name and you’re rolling.

The Four Principles Of OOP

Ok, now we know what object oriented programming looks like and why it’s useful. Let’s move onto some OOP terminology that you can throw out in an interview to get the job and that you should keep in mind as you’re writing clean, reusable code.

1. Encapsulation
We’ve discussed this concept already, so let’s put a name to it. Encapsulation refers to the grouping of related functions and properties together to connect concepts and create DRY (do not repeat yourself) code.

Remember when we looked at the block of code where everything was declared in the global scope? Encapsulation fixes the problems that can bring by housing related code in one data structure (be that a class or an object):

\"\"

2. Abstraction
We haven’t discussed this much yet but the concept of abstraction means to hide the complexity of your code or not allow the alteration of certain fields. You can think of this like any mechanical device (ex. a radio, microwave, TV, etc.). When you want to turn any of those things on, there is a mechanism to do so (ex. push a button). The actual mechanical functions that are performed to turn the appliance on within the device are not known to most of us and, frankly, we don’t much care as long as the thing turns on. The user has those processes obscured by the simplicity of the button push. That concept can also be applied to object oriented programming as abstraction.

Abstraction can be achieved in code through getters and setters which allow a user to only access or update certain fields in structured ways. We won’t talk much about how those work specifically here but managing how a user can see and make changes to the fields in your code makes a big difference in safeguarding the functionality of what you’ve written.

\"\"

In the above example, we can see that the Shape class on the left has no access methods which encourages direct manipulation of the constructor. In contrast, the example on the right has specific methods that are written to get and set the number of sides a shape might have. The functionality is a bit simplistic for the sake of explanation there, but you can also add in checks to ensure things like the number of sides provided is actually a number and not another datatype.

3. Inheritance
Inheritance simply refers to child classes having the attributes of their parent classes passed down to them. This allows a programmer to write less code since methods that have already been written for a parent class don’t need to be rewritten for a child class to have access to them.

\"\"

In the example above, a Person class is declared on the left with a method called introduceYourself() included on it. We can see on the right that a Student class is created which extends the Person class. This means that all of the functionality that the Person class has, the Student class also now has which we can see in action with the sayNameAndGrade() method on the Student class which makes use of the Person class’s introduceYourself() method without redefining it.

4. Polymorphism
Within the concept of polymorphism, the functionality of a child/parent chain of classes is determined at runtime. An example of polymorphism could be the method of a child class overriding the same method on a parent class. As the name suggests, through polymorphism object functionality can take “many forms”. The interaction between parent and children classes can be as simple as the child class inheriting functionality from the parent class or it can be more complex with overlapping methods or child functionality overriding that of the parent. This complexity is called polymorphism and it gives a developer more options in terms of what choices to make with the many available tools a chain or family of classes provides.

\"\"

We can see a simple example above with the code on the left defining a Shape class. That class has a method on it called calcArea(). On the right, we see that Rectangle and Triangle classes extend the Shape class. These two child classes also each have a calcArea() method. When we create a Triangle instance and call calcArea() on it, it’s not the Shape's calcArea() function that is invoked, but the Triangle's. This is a simple example, but it does illustrate the flexibility inherent in working with classes that we call polymorphism.

Alright! That’s it! The last bit of wisdom I’ll leave you with is a way to remember object oriented programming terminology so you can always be ready to write better code or to ace an interview:

Abstraction

Polymorphism
Inheritance
Encapsulation

Additional Resources

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

\"\"

A Brief Introduction to Object Oriented Programming was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

","media":{}},{"id":"https://medium.com/p/17f6e29a07ba","title":"A Guide to Immediately Invoked Function Expressions (IIFE)","link":"https://medium.com/@spencer.attick/a-guide-to-immediately-invoked-function-expressions-iife-17f6e29a07ba?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1681325789000,"created":1681325789000,"category":["javascript","security","code","software-development","technology"],"content":"
\"\"
Anne Nygård for Unsplash

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log(\"Hello, I am a self-invoking arrow function!\");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

\"\"

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = 'secret';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log(\"Returning cached data\");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log(\"Fetching data from server\");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

\"\"","enclosures":[],"content_encoded":"
\"\"
Anne Nygård for Unsplash

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log(\"Hello, I am a self-invoking arrow function!\");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

\"\"

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = 'secret';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log(\"Returning cached data\");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log(\"Fetching data from server\");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData(\"https://jsonplaceholder.typicode.com/posts/1\")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

\"\"","media":{}},{"id":"https://medium.com/p/c3214c897b4c","title":"Project: Script to Automatically Update Resources in my Portfolio","link":"https://medium.com/@spencer.attick/project-script-to-automatically-update-resources-in-my-portfolio-c3214c897b4c?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1681220946000,"created":1681220946000,"category":["portfolio","nodejs","software-development","javascript","script"],"content":"
\"\"

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

\"\"","enclosures":[],"content_encoded":"
\"\"

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

\"\"","media":{}},{"id":"https://medium.com/p/a4c2e35b2558","title":"Project: Creating Code Examples for Segment’s Server-Side Libraries","link":"https://medium.com/@spencer.attick/project-creating-code-examples-for-all-of-segments-server-side-libraries-a4c2e35b2558?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1680791081000,"created":1680791081000,"category":["python","segment","servers","ruby","nodejs"],"content":"
\"\"

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

\"\"

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

\"\"

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

\"\"

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

\"\"","enclosures":[],"content_encoded":"
\"\"

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

\"\"

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

\"\"

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

\"\"

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

\"\"","media":{}},{"id":"https://medium.com/p/fc36c10c02e5","title":"Project: Story Points","link":"https://medium.com/@spencer.attick/project-story-points-fc36c10c02e5?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1680618193000,"created":1680618193000,"category":["javascript","support","optimization","zendesk","story-points"],"content":"
\"\"

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

\"\"

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

\"\"

From there, logic is in place to add points up based on which fields are used:

\"\"

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

\"\"

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

\"\"

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

\"\"","enclosures":[],"content_encoded":"
\"\"

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

\"\"

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

\"\"

From there, logic is in place to add points up based on which fields are used:

\"\"

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

\"\"

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

\"\"

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

\"\"","media":{}},{"id":"https://medium.com/p/f5efe0afd046","title":"Project: Updating Segment’s analytics-node Library to Include TypeScript","link":"https://medium.com/@spencer.attick/project-updating-segments-analytics-node-library-to-include-typescript-f5efe0afd046?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1678993785000,"created":1678993785000,"category":["segment","codecademy","node","typescript"],"content":"
\"\"

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

\"\"","enclosures":[],"content_encoded":"
\"\"

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

\"\"","media":{}},{"id":"https://medium.com/p/9251d1438121","title":"Project: Segment Source Function — Auth0","link":"https://medium.com/@spencer.attick/project-segment-source-function-auth0-9251d1438121?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1678737033000,"created":1678737033000,"category":["auth0","javascript","node","segment"],"content":"

Project: Segment Source Function — Auth0

\"\"

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

\"\"","enclosures":[],"content_encoded":"

Project: Segment Source Function — Auth0

\"\"

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

\"\"","media":{}},{"id":"https://medium.com/p/7f03b1b9e1b9","title":"Scope, Hoisting, and Closure in Javascript","link":"https://medium.com/@spencer.attick/scope-and-hoisting-and-closure-in-javascript-7f03b1b9e1b9?source=rss-e5dc359f27c2------2","author":"Spencer Attick","published":1675365832000,"created":1675365832000,"category":["javascript-tips","hoisting","javascript","scopes","closure"],"content":"
\"\"
Michael Surazhsky for Unsplash

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

\"\"

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

\"\"

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

\"\"","enclosures":[],"content_encoded":"
\"\"
Michael Surazhsky for Unsplash

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

\"\"

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

\"\"

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

\"\"","media":{}}]} \ No newline at end of file diff --git a/dist/assets/index-049fdd1d.js b/dist/assets/index-049fdd1d.js deleted file mode 100644 index 6b11bd8..0000000 --- a/dist/assets/index-049fdd1d.js +++ /dev/null @@ -1,863 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const a of o)if(a.type==="childList")for(const i of a.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(o){const a={};return o.integrity&&(a.integrity=o.integrity),o.referrerpolicy&&(a.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?a.credentials="include":o.crossorigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(o){if(o.ep)return;o.ep=!0;const a=n(o);fetch(o.href,a)}})();function $h(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var No={},qh={get exports(){return No},set exports(e){No=e}},ia={},we={},Zh={get exports(){return we},set exports(e){we=e}},O={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var zr=Symbol.for("react.element"),Qh=Symbol.for("react.portal"),Kh=Symbol.for("react.fragment"),Xh=Symbol.for("react.strict_mode"),ef=Symbol.for("react.profiler"),tf=Symbol.for("react.provider"),nf=Symbol.for("react.context"),rf=Symbol.for("react.forward_ref"),of=Symbol.for("react.suspense"),af=Symbol.for("react.memo"),sf=Symbol.for("react.lazy"),Il=Symbol.iterator;function lf(e){return e===null||typeof e!="object"?null:(e=Il&&e[Il]||e["@@iterator"],typeof e=="function"?e:null)}var ac={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},ic=Object.assign,sc={};function Rn(e,t,n){this.props=e,this.context=t,this.refs=sc,this.updater=n||ac}Rn.prototype.isReactComponent={};Rn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Rn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function lc(){}lc.prototype=Rn.prototype;function ms(e,t,n){this.props=e,this.context=t,this.refs=sc,this.updater=n||ac}var gs=ms.prototype=new lc;gs.constructor=ms;ic(gs,Rn.prototype);gs.isPureReactComponent=!0;var xl=Array.isArray,uc=Object.prototype.hasOwnProperty,ys={current:null},cc={key:!0,ref:!0,__self:!0,__source:!0};function dc(e,t,n){var r,o={},a=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(a=""+t.key),t)uc.call(t,r)&&!cc.hasOwnProperty(r)&&(o[r]=t[r]);var s=arguments.length-2;if(s===1)o.children=n;else if(1>>1,ne=E[Z];if(0>>1;Zo(Aa,z))Fto(Vr,Aa)?(E[Z]=Vr,E[Ft]=z,Z=Ft):(E[Z]=Aa,E[Mt]=z,Z=Mt);else if(Fto(Vr,z))E[Z]=Vr,E[Ft]=z,Z=Ft;else break e}}return _}function o(E,_){var z=E.sortIndex-_.sortIndex;return z!==0?z:E.id-_.id}if(typeof performance=="object"&&typeof performance.now=="function"){var a=performance;e.unstable_now=function(){return a.now()}}else{var i=Date,s=i.now();e.unstable_now=function(){return i.now()-s}}var l=[],u=[],d=1,p=null,m=3,g=!1,v=!1,b=!1,N=typeof setTimeout=="function"?setTimeout:null,h=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function f(E){for(var _=n(u);_!==null;){if(_.callback===null)r(u);else if(_.startTime<=E)r(u),_.sortIndex=_.expirationTime,t(l,_);else break;_=n(u)}}function y(E){if(b=!1,f(E),!v)if(n(l)!==null)v=!0,Ca(k);else{var _=n(u);_!==null&&Pa(y,_.startTime-E)}}function k(E,_){v=!1,b&&(b=!1,h(P),P=-1),g=!0;var z=m;try{for(f(_),p=n(l);p!==null&&(!(p.expirationTime>_)||E&&!Me());){var Z=p.callback;if(typeof Z=="function"){p.callback=null,m=p.priorityLevel;var ne=Z(p.expirationTime<=_);_=e.unstable_now(),typeof ne=="function"?p.callback=ne:p===n(l)&&r(l),f(_)}else r(l);p=n(l)}if(p!==null)var Wr=!0;else{var Mt=n(u);Mt!==null&&Pa(y,Mt.startTime-_),Wr=!1}return Wr}finally{p=null,m=z,g=!1}}var x=!1,T=null,P=-1,F=5,R=-1;function Me(){return!(e.unstable_now()-RE||125Z?(E.sortIndex=z,t(u,E),n(l)===null&&E===n(u)&&(b?(h(P),P=-1):b=!0,Pa(y,z-Z))):(E.sortIndex=ne,t(l,E),v||g||(v=!0,Ca(k))),E},e.unstable_shouldYield=Me,e.unstable_wrapCallback=function(E){var _=m;return function(){var z=m;m=_;try{return E.apply(this,arguments)}finally{m=z}}}})(fc);(function(e){e.exports=fc})(bf);/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var pc=we,Ee=li;function w(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ui=Object.prototype.hasOwnProperty,kf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,El={},Cl={};function Sf(e){return ui.call(Cl,e)?!0:ui.call(El,e)?!1:kf.test(e)?Cl[e]=!0:(El[e]=!0,!1)}function If(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function xf(e,t,n,r){if(t===null||typeof t>"u"||If(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ge(e,t,n,r,o,a,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=o,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=i}var se={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){se[e]=new ge(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];se[t]=new ge(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){se[e]=new ge(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){se[e]=new ge(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){se[e]=new ge(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){se[e]=new ge(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){se[e]=new ge(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){se[e]=new ge(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){se[e]=new ge(e,5,!1,e.toLowerCase(),null,!1,!1)});var ws=/[\-:]([a-z])/g;function bs(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ws,bs);se[t]=new ge(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ws,bs);se[t]=new ge(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ws,bs);se[t]=new ge(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){se[e]=new ge(e,1,!1,e.toLowerCase(),null,!1,!1)});se.xlinkHref=new ge("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){se[e]=new ge(e,1,!1,e.toLowerCase(),null,!0,!0)});function ks(e,t,n,r){var o=se.hasOwnProperty(t)?se[t]:null;(o!==null?o.type!==0:r||!(2s||o[i]!==a[s]){var l=` -`+o[i].replace(" at new "," at ");return e.displayName&&l.includes("")&&(l=l.replace("",e.displayName)),l}while(1<=i&&0<=s);break}}}finally{ja=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?qn(e):""}function Tf(e){switch(e.tag){case 5:return qn(e.type);case 16:return qn("Lazy");case 13:return qn("Suspense");case 19:return qn("SuspenseList");case 0:case 2:case 15:return e=za(e.type,!1),e;case 11:return e=za(e.type.render,!1),e;case 1:return e=za(e.type,!0),e;default:return""}}function fi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case on:return"Fragment";case rn:return"Portal";case ci:return"Profiler";case Ss:return"StrictMode";case di:return"Suspense";case hi:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case yc:return(e.displayName||"Context")+".Consumer";case gc:return(e._context.displayName||"Context")+".Provider";case Is:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case xs:return t=e.displayName||null,t!==null?t:fi(e.type)||"Memo";case ft:t=e._payload,e=e._init;try{return fi(e(t))}catch{}}return null}function Ef(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return fi(t);case 8:return t===Ss?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Pt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function wc(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Cf(e){var t=wc(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var o=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(i){r=""+i,a.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Br(e){e._valueTracker||(e._valueTracker=Cf(e))}function bc(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=wc(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function jo(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function pi(e,t){var n=t.checked;return J({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Al(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Pt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function kc(e,t){t=t.checked,t!=null&&ks(e,"checked",t,!1)}function mi(e,t){kc(e,t);var n=Pt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?gi(e,t.type,n):t.hasOwnProperty("defaultValue")&&gi(e,t.type,Pt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Nl(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function gi(e,t,n){(t!=="number"||jo(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Zn=Array.isArray;function wn(e,t,n,r){if(e=e.options,t){t={};for(var o=0;o"+t.valueOf().toString()+"",t=Yr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function hr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var er={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Pf=["Webkit","ms","Moz","O"];Object.keys(er).forEach(function(e){Pf.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),er[t]=er[e]})});function Tc(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||er.hasOwnProperty(e)&&er[e]?(""+t).trim():t+"px"}function Ec(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,o=Tc(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,o):e[n]=o}}var Af=J({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function wi(e,t){if(t){if(Af[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(w(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(w(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(w(61))}if(t.style!=null&&typeof t.style!="object")throw Error(w(62))}}function bi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ki=null;function Ts(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Si=null,bn=null,kn=null;function zl(e){if(e=Mr(e)){if(typeof Si!="function")throw Error(w(280));var t=e.stateNode;t&&(t=ha(t),Si(e.stateNode,e.type,t))}}function Cc(e){bn?kn?kn.push(e):kn=[e]:bn=e}function Pc(){if(bn){var e=bn,t=kn;if(kn=bn=null,zl(e),t)for(e=0;e>>=0,e===0?32:31-(Hf(e)/Wf|0)|0}var Jr=64,$r=4194304;function Qn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Mo(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,o=e.suspendedLanes,a=e.pingedLanes,i=n&268435455;if(i!==0){var s=i&~o;s!==0?r=Qn(s):(a&=i,a!==0&&(r=Qn(a)))}else i=n&~o,i!==0?r=Qn(i):a!==0&&(r=Qn(a));if(r===0)return 0;if(t!==0&&t!==r&&!(t&o)&&(o=r&-r,a=t&-t,o>=a||o===16&&(a&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Rr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ve(t),e[t]=n}function Bf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=nr),Vl=String.fromCharCode(32),Ul=!1;function $c(e,t){switch(e){case"keyup":return wp.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function qc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var an=!1;function kp(e,t){switch(e){case"compositionend":return qc(t);case"keypress":return t.which!==32?null:(Ul=!0,Vl);case"textInput":return e=t.data,e===Vl&&Ul?null:e;default:return null}}function Sp(e,t){if(an)return e==="compositionend"||!zs&&$c(e,t)?(e=Yc(),wo=Ns=yt=null,an=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Jl(n)}}function Xc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Xc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ed(){for(var e=window,t=jo();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=jo(e.document)}return t}function Rs(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function _p(e){var t=ed(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Xc(n.ownerDocument.documentElement,n)){if(r!==null&&Rs(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var o=n.textContent.length,a=Math.min(r.start,o);r=r.end===void 0?a:Math.min(r.end,o),!e.extend&&a>r&&(o=r,r=a,a=o),o=$l(n,a);var i=$l(n,r);o&&i&&(e.rangeCount!==1||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(o.node,o.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,sn=null,Pi=null,or=null,Ai=!1;function ql(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ai||sn==null||sn!==jo(r)||(r=sn,"selectionStart"in r&&Rs(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),or&&vr(or,r)||(or=r,r=Do(Pi,"onSelect"),0cn||(e.current=Oi[cn],Oi[cn]=null,cn--)}function D(e,t){cn++,Oi[cn]=e.current,e.current=t}var At={},he=Rt(At),be=Rt(!1),Jt=At;function Cn(e,t){var n=e.type.contextTypes;if(!n)return At;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var o={},a;for(a in n)o[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=o),o}function ke(e){return e=e.childContextTypes,e!=null}function Wo(){V(be),V(he)}function nu(e,t,n){if(he.current!==At)throw Error(w(168));D(he,t),D(be,n)}function ud(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var o in r)if(!(o in t))throw Error(w(108,Ef(e)||"Unknown",o));return J({},n,r)}function Vo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||At,Jt=he.current,D(he,e),D(be,be.current),!0}function ru(e,t,n){var r=e.stateNode;if(!r)throw Error(w(169));n?(e=ud(e,t,Jt),r.__reactInternalMemoizedMergedChildContext=e,V(be),V(he),D(he,e)):V(be),D(be,n)}var Ke=null,fa=!1,Ja=!1;function cd(e){Ke===null?Ke=[e]:Ke.push(e)}function Up(e){fa=!0,cd(e)}function Ot(){if(!Ja&&Ke!==null){Ja=!0;var e=0,t=L;try{var n=Ke;for(L=1;e>=i,o-=i,Xe=1<<32-Ve(t)+o|n<P?(F=T,T=null):F=T.sibling;var R=m(h,T,f[P],y);if(R===null){T===null&&(T=F);break}e&&T&&R.alternate===null&&t(h,T),c=a(R,c,P),x===null?k=R:x.sibling=R,x=R,T=F}if(P===f.length)return n(h,T),G&&Lt(h,P),k;if(T===null){for(;PP?(F=T,T=null):F=T.sibling;var Me=m(h,T,R.value,y);if(Me===null){T===null&&(T=F);break}e&&T&&Me.alternate===null&&t(h,T),c=a(Me,c,P),x===null?k=Me:x.sibling=Me,x=Me,T=F}if(R.done)return n(h,T),G&&Lt(h,P),k;if(T===null){for(;!R.done;P++,R=f.next())R=p(h,R.value,y),R!==null&&(c=a(R,c,P),x===null?k=R:x.sibling=R,x=R);return G&&Lt(h,P),k}for(T=r(h,T);!R.done;P++,R=f.next())R=g(T,h,P,R.value,y),R!==null&&(e&&R.alternate!==null&&T.delete(R.key===null?P:R.key),c=a(R,c,P),x===null?k=R:x.sibling=R,x=R);return e&&T.forEach(function(Dn){return t(h,Dn)}),G&&Lt(h,P),k}function N(h,c,f,y){if(typeof f=="object"&&f!==null&&f.type===on&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case Gr:e:{for(var k=f.key,x=c;x!==null;){if(x.key===k){if(k=f.type,k===on){if(x.tag===7){n(h,x.sibling),c=o(x,f.props.children),c.return=h,h=c;break e}}else if(x.elementType===k||typeof k=="object"&&k!==null&&k.$$typeof===ft&&cu(k)===x.type){n(h,x.sibling),c=o(x,f.props),c.ref=Yn(h,x,f),c.return=h,h=c;break e}n(h,x);break}else t(h,x);x=x.sibling}f.type===on?(c=Yt(f.props.children,h.mode,y,f.key),c.return=h,h=c):(y=Co(f.type,f.key,f.props,null,h.mode,y),y.ref=Yn(h,c,f),y.return=h,h=y)}return i(h);case rn:e:{for(x=f.key;c!==null;){if(c.key===x)if(c.tag===4&&c.stateNode.containerInfo===f.containerInfo&&c.stateNode.implementation===f.implementation){n(h,c.sibling),c=o(c,f.children||[]),c.return=h,h=c;break e}else{n(h,c);break}else t(h,c);c=c.sibling}c=ti(f,h.mode,y),c.return=h,h=c}return i(h);case ft:return x=f._init,N(h,c,x(f._payload),y)}if(Zn(f))return v(h,c,f,y);if(Wn(f))return b(h,c,f,y);to(h,f)}return typeof f=="string"&&f!==""||typeof f=="number"?(f=""+f,c!==null&&c.tag===6?(n(h,c.sibling),c=o(c,f),c.return=h,h=c):(n(h,c),c=ei(f,h.mode,y),c.return=h,h=c),i(h)):n(h,c)}return N}var An=vd(!0),wd=vd(!1),Fr={},Ze=Rt(Fr),Sr=Rt(Fr),Ir=Rt(Fr);function Vt(e){if(e===Fr)throw Error(w(174));return e}function Us(e,t){switch(D(Ir,t),D(Sr,e),D(Ze,Fr),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:vi(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=vi(t,e)}V(Ze),D(Ze,t)}function Nn(){V(Ze),V(Sr),V(Ir)}function bd(e){Vt(Ir.current);var t=Vt(Ze.current),n=vi(t,e.type);t!==n&&(D(Sr,e),D(Ze,n))}function Gs(e){Sr.current===e&&(V(Ze),V(Sr))}var B=Rt(0);function $o(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var $a=[];function Bs(){for(var e=0;e<$a.length;e++)$a[e]._workInProgressVersionPrimary=null;$a.length=0}var So=ut.ReactCurrentDispatcher,qa=ut.ReactCurrentBatchConfig,qt=0,Y=null,X=null,re=null,qo=!1,ar=!1,xr=0,Bp=0;function le(){throw Error(w(321))}function Ys(e,t){if(t===null)return!1;for(var n=0;nn?n:4,e(!0);var r=qa.transition;qa.transition={};try{e(!1),t()}finally{L=n,qa.transition=r}}function Md(){return Oe().memoizedState}function Jp(e,t,n){var r=Et(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Fd(e))Ld(t,n);else if(n=pd(e,t,n,r),n!==null){var o=pe();Ue(n,e,r,o),Dd(n,t,r)}}function $p(e,t,n){var r=Et(e),o={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Fd(e))Ld(t,o);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var i=t.lastRenderedState,s=a(i,n);if(o.hasEagerState=!0,o.eagerState=s,Ge(s,i)){var l=t.interleaved;l===null?(o.next=o,Ws(t)):(o.next=l.next,l.next=o),t.interleaved=o;return}}catch{}finally{}n=pd(e,t,o,r),n!==null&&(o=pe(),Ue(n,e,r,o),Dd(n,t,r))}}function Fd(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function Ld(e,t){ar=qo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Dd(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Cs(e,n)}}var Zo={readContext:Re,useCallback:le,useContext:le,useEffect:le,useImperativeHandle:le,useInsertionEffect:le,useLayoutEffect:le,useMemo:le,useReducer:le,useRef:le,useState:le,useDebugValue:le,useDeferredValue:le,useTransition:le,useMutableSource:le,useSyncExternalStore:le,useId:le,unstable_isNewReconciler:!1},qp={readContext:Re,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:Re,useEffect:hu,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Io(4194308,4,_d.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Io(4194308,4,e,t)},useInsertionEffect:function(e,t){return Io(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Jp.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:du,useDebugValue:Zs,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=du(!1),t=e[0];return e=Yp.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,o=Ye();if(G){if(n===void 0)throw Error(w(407));n=n()}else{if(n=t(),oe===null)throw Error(w(349));qt&30||Id(r,t,n)}o.memoizedState=n;var a={value:n,getSnapshot:t};return o.queue=a,hu(Td.bind(null,r,a,e),[e]),r.flags|=2048,Er(9,xd.bind(null,r,a,n,t),void 0,null),n},useId:function(){var e=Ye(),t=oe.identifierPrefix;if(G){var n=et,r=Xe;n=(r&~(1<<32-Ve(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=xr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Je]=t,e[kr]=r,$d(e,t,!1,!1),t.stateNode=e;e:{switch(i=bi(n,r),n){case"dialog":H("cancel",e),H("close",e),o=r;break;case"iframe":case"object":case"embed":H("load",e),o=r;break;case"video":case"audio":for(o=0;ojn&&(t.flags|=128,r=!0,Jn(a,!1),t.lanes=4194304)}else{if(!r)if(e=$o(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Jn(a,!0),a.tail===null&&a.tailMode==="hidden"&&!i.alternate&&!G)return ue(t),null}else 2*Q()-a.renderingStartTime>jn&&n!==1073741824&&(t.flags|=128,r=!0,Jn(a,!1),t.lanes=4194304);a.isBackwards?(i.sibling=t.child,t.child=i):(n=a.last,n!==null?n.sibling=i:t.child=i,a.last=i)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=Q(),t.sibling=null,n=B.current,D(B,r?n&1|2:n&1),t):(ue(t),null);case 22:case 23:return nl(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ie&1073741824&&(ue(t),t.subtreeFlags&6&&(t.flags|=8192)):ue(t),null;case 24:return null;case 25:return null}throw Error(w(156,t.tag))}function rm(e,t){switch(Ms(t),t.tag){case 1:return ke(t.type)&&Wo(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Nn(),V(be),V(he),Bs(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Gs(t),null;case 13:if(V(B),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(w(340));Pn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(B),null;case 4:return Nn(),null;case 10:return Hs(t.type._context),null;case 22:case 23:return nl(),null;case 24:return null;default:return null}}var ro=!1,ce=!1,om=typeof WeakSet=="function"?WeakSet:Set,I=null;function pn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){$(e,t,r)}else n.current=null}function Ji(e,t,n){try{n()}catch(r){$(e,t,r)}}var ku=!1;function am(e,t){if(Ni=Fo,e=ed(),Rs(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var o=r.anchorOffset,a=r.focusNode;r=r.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break e}var i=0,s=-1,l=-1,u=0,d=0,p=e,m=null;t:for(;;){for(var g;p!==n||o!==0&&p.nodeType!==3||(s=i+o),p!==a||r!==0&&p.nodeType!==3||(l=i+r),p.nodeType===3&&(i+=p.nodeValue.length),(g=p.firstChild)!==null;)m=p,p=g;for(;;){if(p===e)break t;if(m===n&&++u===o&&(s=i),m===a&&++d===r&&(l=i),(g=p.nextSibling)!==null)break;p=m,m=p.parentNode}p=g}n=s===-1||l===-1?null:{start:s,end:l}}else n=null}n=n||{start:0,end:0}}else n=null;for(_i={focusedElem:e,selectionRange:n},Fo=!1,I=t;I!==null;)if(t=I,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,I=e;else for(;I!==null;){t=I;try{var v=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(v!==null){var b=v.memoizedProps,N=v.memoizedState,h=t.stateNode,c=h.getSnapshotBeforeUpdate(t.elementType===t.type?b:Le(t.type,b),N);h.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(w(163))}}catch(y){$(t,t.return,y)}if(e=t.sibling,e!==null){e.return=t.return,I=e;break}I=t.return}return v=ku,ku=!1,v}function ir(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var o=r=r.next;do{if((o.tag&e)===e){var a=o.destroy;o.destroy=void 0,a!==void 0&&Ji(t,n,a)}o=o.next}while(o!==r)}}function ga(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function $i(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Qd(e){var t=e.alternate;t!==null&&(e.alternate=null,Qd(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Je],delete t[kr],delete t[Ri],delete t[Wp],delete t[Vp])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Kd(e){return e.tag===5||e.tag===3||e.tag===4}function Su(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Kd(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function qi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Ho));else if(r!==4&&(e=e.child,e!==null))for(qi(e,t,n),e=e.sibling;e!==null;)qi(e,t,n),e=e.sibling}function Zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Zi(e,t,n),e=e.sibling;e!==null;)Zi(e,t,n),e=e.sibling}var ae=null,De=!1;function dt(e,t,n){for(n=n.child;n!==null;)Xd(e,t,n),n=n.sibling}function Xd(e,t,n){if(qe&&typeof qe.onCommitFiberUnmount=="function")try{qe.onCommitFiberUnmount(la,n)}catch{}switch(n.tag){case 5:ce||pn(n,t);case 6:var r=ae,o=De;ae=null,dt(e,t,n),ae=r,De=o,ae!==null&&(De?(e=ae,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ae.removeChild(n.stateNode));break;case 18:ae!==null&&(De?(e=ae,n=n.stateNode,e.nodeType===8?Ya(e.parentNode,n):e.nodeType===1&&Ya(e,n),gr(e)):Ya(ae,n.stateNode));break;case 4:r=ae,o=De,ae=n.stateNode.containerInfo,De=!0,dt(e,t,n),ae=r,De=o;break;case 0:case 11:case 14:case 15:if(!ce&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){o=r=r.next;do{var a=o,i=a.destroy;a=a.tag,i!==void 0&&(a&2||a&4)&&Ji(n,t,i),o=o.next}while(o!==r)}dt(e,t,n);break;case 1:if(!ce&&(pn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(s){$(n,t,s)}dt(e,t,n);break;case 21:dt(e,t,n);break;case 22:n.mode&1?(ce=(r=ce)||n.memoizedState!==null,dt(e,t,n),ce=r):dt(e,t,n);break;default:dt(e,t,n)}}function Iu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new om),t.forEach(function(r){var o=pm.bind(null,e,r);n.has(r)||(n.add(r),r.then(o,o))})}}function Fe(e,t){var n=t.deletions;if(n!==null)for(var r=0;ro&&(o=i),r&=~a}if(r=o,r=Q()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*sm(r/1960))-r,10e?16:e,vt===null)var r=!1;else{if(e=vt,vt=null,Xo=0,M&6)throw Error(w(331));var o=M;for(M|=4,I=e.current;I!==null;){var a=I,i=a.child;if(I.flags&16){var s=a.deletions;if(s!==null){for(var l=0;lQ()-el?Bt(e,0):Xs|=n),Se(e,t)}function sh(e,t){t===0&&(e.mode&1?(t=$r,$r<<=1,!($r&130023424)&&($r=4194304)):t=1);var n=pe();e=ot(e,t),e!==null&&(Rr(e,t,n),Se(e,n))}function fm(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),sh(e,n)}function pm(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,o=e.memoizedState;o!==null&&(n=o.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(w(314))}r!==null&&r.delete(t),sh(e,n)}var lh;lh=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||be.current)ve=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ve=!1,tm(e,t,n);ve=!!(e.flags&131072)}else ve=!1,G&&t.flags&1048576&&dd(t,Go,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;xo(e,t),e=t.pendingProps;var o=Cn(t,he.current);In(t,n),o=Js(null,t,r,e,o,n);var a=$s();return t.flags|=1,typeof o=="object"&&o!==null&&typeof o.render=="function"&&o.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ke(r)?(a=!0,Vo(t)):a=!1,t.memoizedState=o.state!==null&&o.state!==void 0?o.state:null,Vs(t),o.updater=pa,t.stateNode=o,o._reactInternals=t,Hi(t,r,e,n),t=Ui(null,t,r,!0,a,n)):(t.tag=0,G&&a&&Os(t),fe(null,t,o,n),t=t.child),t;case 16:r=t.elementType;e:{switch(xo(e,t),e=t.pendingProps,o=r._init,r=o(r._payload),t.type=r,o=t.tag=gm(r),e=Le(r,e),o){case 0:t=Vi(null,t,r,e,n);break e;case 1:t=vu(null,t,r,e,n);break e;case 11:t=gu(null,t,r,e,n);break e;case 14:t=yu(null,t,r,Le(r.type,e),n);break e}throw Error(w(306,r,""))}return t;case 0:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Le(r,o),Vi(e,t,r,o,n);case 1:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Le(r,o),vu(e,t,r,o,n);case 3:e:{if(Bd(t),e===null)throw Error(w(387));r=t.pendingProps,a=t.memoizedState,o=a.element,md(e,t),Jo(t,r,null,n);var i=t.memoizedState;if(r=i.element,a.isDehydrated)if(a={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=a,t.memoizedState=a,t.flags&256){o=_n(Error(w(423)),t),t=wu(e,t,r,n,o);break e}else if(r!==o){o=_n(Error(w(424)),t),t=wu(e,t,r,n,o);break e}else for(xe=It(t.stateNode.containerInfo.firstChild),Te=t,G=!0,He=null,n=wd(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Pn(),r===o){t=at(e,t,n);break e}fe(e,t,r,n)}t=t.child}return t;case 5:return bd(t),e===null&&Fi(t),r=t.type,o=t.pendingProps,a=e!==null?e.memoizedProps:null,i=o.children,ji(r,o)?i=null:a!==null&&ji(r,a)&&(t.flags|=32),Gd(e,t),fe(e,t,i,n),t.child;case 6:return e===null&&Fi(t),null;case 13:return Yd(e,t,n);case 4:return Us(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=An(t,null,r,n):fe(e,t,r,n),t.child;case 11:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Le(r,o),gu(e,t,r,o,n);case 7:return fe(e,t,t.pendingProps,n),t.child;case 8:return fe(e,t,t.pendingProps.children,n),t.child;case 12:return fe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,o=t.pendingProps,a=t.memoizedProps,i=o.value,D(Bo,r._currentValue),r._currentValue=i,a!==null)if(Ge(a.value,i)){if(a.children===o.children&&!be.current){t=at(e,t,n);break e}}else for(a=t.child,a!==null&&(a.return=t);a!==null;){var s=a.dependencies;if(s!==null){i=a.child;for(var l=s.firstContext;l!==null;){if(l.context===r){if(a.tag===1){l=tt(-1,n&-n),l.tag=2;var u=a.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}a.lanes|=n,l=a.alternate,l!==null&&(l.lanes|=n),Li(a.return,n,t),s.lanes|=n;break}l=l.next}}else if(a.tag===10)i=a.type===t.type?null:a.child;else if(a.tag===18){if(i=a.return,i===null)throw Error(w(341));i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),Li(i,n,t),i=a.sibling}else i=a.child;if(i!==null)i.return=a;else for(i=a;i!==null;){if(i===t){i=null;break}if(a=i.sibling,a!==null){a.return=i.return,i=a;break}i=i.return}a=i}fe(e,t,o.children,n),t=t.child}return t;case 9:return o=t.type,r=t.pendingProps.children,In(t,n),o=Re(o),r=r(o),t.flags|=1,fe(e,t,r,n),t.child;case 14:return r=t.type,o=Le(r,t.pendingProps),o=Le(r.type,o),yu(e,t,r,o,n);case 15:return Vd(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Le(r,o),xo(e,t),t.tag=1,ke(r)?(e=!0,Vo(t)):e=!1,In(t,n),yd(t,r,o),Hi(t,r,o,n),Ui(null,t,r,!0,e,n);case 19:return Jd(e,t,n);case 22:return Ud(e,t,n)}throw Error(w(156,t.tag))};function uh(e,t){return Oc(e,t)}function mm(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function je(e,t,n,r){return new mm(e,t,n,r)}function ol(e){return e=e.prototype,!(!e||!e.isReactComponent)}function gm(e){if(typeof e=="function")return ol(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Is)return 11;if(e===xs)return 14}return 2}function Ct(e,t){var n=e.alternate;return n===null?(n=je(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Co(e,t,n,r,o,a){var i=2;if(r=e,typeof e=="function")ol(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case on:return Yt(n.children,o,a,t);case Ss:i=8,o|=8;break;case ci:return e=je(12,n,t,o|2),e.elementType=ci,e.lanes=a,e;case di:return e=je(13,n,t,o),e.elementType=di,e.lanes=a,e;case hi:return e=je(19,n,t,o),e.elementType=hi,e.lanes=a,e;case vc:return va(n,o,a,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case gc:i=10;break e;case yc:i=9;break e;case Is:i=11;break e;case xs:i=14;break e;case ft:i=16,r=null;break e}throw Error(w(130,e==null?e:typeof e,""))}return t=je(i,n,t,o),t.elementType=e,t.type=r,t.lanes=a,t}function Yt(e,t,n,r){return e=je(7,e,r,t),e.lanes=n,e}function va(e,t,n,r){return e=je(22,e,r,t),e.elementType=vc,e.lanes=n,e.stateNode={isHidden:!1},e}function ei(e,t,n){return e=je(6,e,null,t),e.lanes=n,e}function ti(e,t,n){return t=je(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function ym(e,t,n,r,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Oa(0),this.expirationTimes=Oa(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Oa(0),this.identifierPrefix=r,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function al(e,t,n,r,o,a,i,s,l){return e=new ym(e,t,n,s,l),t===1?(t=1,a===!0&&(t|=8)):t=0,a=je(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Vs(a),e}function vm(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(n){console.error(n)}}t(),e.exports=Ce})(wf);var _u=si;_o.createRoot=_u.createRoot,_o.hydrateRoot=_u.hydrateRoot;function Im(){return de("div",{id:"intro",title:"Spencer road cycling up a hill near the Pacific Ocean",children:[A("h1",{children:A("span",{className:"underline",children:"SPENCER ATTICK"})}),A("h3",{children:A("span",{className:"underline",children:"he/him"})}),A("p",{children:A("span",{className:"underline",children:"Software Engineer"})})]})}const xm="/assets/aboutMe-255da658.png";function Tm(){return A("div",{id:"about-me",children:de("p",{children:[A("img",{src:xm,alt:"Spencer wearing sunglasses sitting with his dog - a Doberman Shephard mix"}),"Hi, I'm Spencer! I have a passion for writing great code and solving technical problems. I've worked in the tech industry for over four years now and have learned a ton along the way. Keep scrolling to take a look at what I've been up to!"]})})}function ju(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function S(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n-1;o--){var a=n[o],i=(a.tagName||"").toUpperCase();["STYLE","LINK"].indexOf(i)>-1&&(r=a)}return U.head.insertBefore(t,r),e}}var Zm="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";function jr(){for(var e=12,t="";e-- >0;)t+=Zm[Math.random()*62|0];return t}function Fn(e){for(var t=[],n=(e||[]).length>>>0;n--;)t[n]=e[n];return t}function pl(e){return e.classList?Fn(e.classList):(e.getAttribute("class")||"").split(" ").filter(function(t){return t})}function xh(e){return"".concat(e).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function Qm(e){return Object.keys(e||{}).reduce(function(t,n){return t+"".concat(n,'="').concat(xh(e[n]),'" ')},"").trim()}function Ia(e){return Object.keys(e||{}).reduce(function(t,n){return t+"".concat(n,": ").concat(e[n].trim(),";")},"")}function ml(e){return e.size!==$e.size||e.x!==$e.x||e.y!==$e.y||e.rotate!==$e.rotate||e.flipX||e.flipY}function Km(e){var t=e.transform,n=e.containerWidth,r=e.iconWidth,o={transform:"translate(".concat(n/2," 256)")},a="translate(".concat(t.x*32,", ").concat(t.y*32,") "),i="scale(".concat(t.size/16*(t.flipX?-1:1),", ").concat(t.size/16*(t.flipY?-1:1),") "),s="rotate(".concat(t.rotate," 0 0)"),l={transform:"".concat(a," ").concat(i," ").concat(s)},u={transform:"translate(".concat(r/2*-1," -256)")};return{outer:o,inner:l,path:u}}function Xm(e){var t=e.transform,n=e.width,r=n===void 0?ns:n,o=e.height,a=o===void 0?ns:o,i=e.startCentered,s=i===void 0?!1:i,l="";return s&&yh?l+="translate(".concat(t.x/ht-r/2,"em, ").concat(t.y/ht-a/2,"em) "):s?l+="translate(calc(-50% + ".concat(t.x/ht,"em), calc(-50% + ").concat(t.y/ht,"em)) "):l+="translate(".concat(t.x/ht,"em, ").concat(t.y/ht,"em) "),l+="scale(".concat(t.size/ht*(t.flipX?-1:1),", ").concat(t.size/ht*(t.flipY?-1:1),") "),l+="rotate(".concat(t.rotate,"deg) "),l}var eg=`:root, :host { - --fa-font-solid: normal 900 1em/1 "Font Awesome 6 Solid"; - --fa-font-regular: normal 400 1em/1 "Font Awesome 6 Regular"; - --fa-font-light: normal 300 1em/1 "Font Awesome 6 Light"; - --fa-font-thin: normal 100 1em/1 "Font Awesome 6 Thin"; - --fa-font-duotone: normal 900 1em/1 "Font Awesome 6 Duotone"; - --fa-font-sharp-solid: normal 900 1em/1 "Font Awesome 6 Sharp"; - --fa-font-sharp-regular: normal 400 1em/1 "Font Awesome 6 Sharp"; - --fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands"; -} - -svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa { - overflow: visible; - box-sizing: content-box; -} - -.svg-inline--fa { - display: var(--fa-display, inline-block); - height: 1em; - overflow: visible; - vertical-align: -0.125em; -} -.svg-inline--fa.fa-2xs { - vertical-align: 0.1em; -} -.svg-inline--fa.fa-xs { - vertical-align: 0em; -} -.svg-inline--fa.fa-sm { - vertical-align: -0.0714285705em; -} -.svg-inline--fa.fa-lg { - vertical-align: -0.2em; -} -.svg-inline--fa.fa-xl { - vertical-align: -0.25em; -} -.svg-inline--fa.fa-2xl { - vertical-align: -0.3125em; -} -.svg-inline--fa.fa-pull-left { - margin-right: var(--fa-pull-margin, 0.3em); - width: auto; -} -.svg-inline--fa.fa-pull-right { - margin-left: var(--fa-pull-margin, 0.3em); - width: auto; -} -.svg-inline--fa.fa-li { - width: var(--fa-li-width, 2em); - top: 0.25em; -} -.svg-inline--fa.fa-fw { - width: var(--fa-fw-width, 1.25em); -} - -.fa-layers svg.svg-inline--fa { - bottom: 0; - left: 0; - margin: auto; - position: absolute; - right: 0; - top: 0; -} - -.fa-layers-counter, .fa-layers-text { - display: inline-block; - position: absolute; - text-align: center; -} - -.fa-layers { - display: inline-block; - height: 1em; - position: relative; - text-align: center; - vertical-align: -0.125em; - width: 1em; -} -.fa-layers svg.svg-inline--fa { - -webkit-transform-origin: center center; - transform-origin: center center; -} - -.fa-layers-text { - left: 50%; - top: 50%; - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - -webkit-transform-origin: center center; - transform-origin: center center; -} - -.fa-layers-counter { - background-color: var(--fa-counter-background-color, #ff253a); - border-radius: var(--fa-counter-border-radius, 1em); - box-sizing: border-box; - color: var(--fa-inverse, #fff); - line-height: var(--fa-counter-line-height, 1); - max-width: var(--fa-counter-max-width, 5em); - min-width: var(--fa-counter-min-width, 1.5em); - overflow: hidden; - padding: var(--fa-counter-padding, 0.25em 0.5em); - right: var(--fa-right, 0); - text-overflow: ellipsis; - top: var(--fa-top, 0); - -webkit-transform: scale(var(--fa-counter-scale, 0.25)); - transform: scale(var(--fa-counter-scale, 0.25)); - -webkit-transform-origin: top right; - transform-origin: top right; -} - -.fa-layers-bottom-right { - bottom: var(--fa-bottom, 0); - right: var(--fa-right, 0); - top: auto; - -webkit-transform: scale(var(--fa-layers-scale, 0.25)); - transform: scale(var(--fa-layers-scale, 0.25)); - -webkit-transform-origin: bottom right; - transform-origin: bottom right; -} - -.fa-layers-bottom-left { - bottom: var(--fa-bottom, 0); - left: var(--fa-left, 0); - right: auto; - top: auto; - -webkit-transform: scale(var(--fa-layers-scale, 0.25)); - transform: scale(var(--fa-layers-scale, 0.25)); - -webkit-transform-origin: bottom left; - transform-origin: bottom left; -} - -.fa-layers-top-right { - top: var(--fa-top, 0); - right: var(--fa-right, 0); - -webkit-transform: scale(var(--fa-layers-scale, 0.25)); - transform: scale(var(--fa-layers-scale, 0.25)); - -webkit-transform-origin: top right; - transform-origin: top right; -} - -.fa-layers-top-left { - left: var(--fa-left, 0); - right: auto; - top: var(--fa-top, 0); - -webkit-transform: scale(var(--fa-layers-scale, 0.25)); - transform: scale(var(--fa-layers-scale, 0.25)); - -webkit-transform-origin: top left; - transform-origin: top left; -} - -.fa-1x { - font-size: 1em; -} - -.fa-2x { - font-size: 2em; -} - -.fa-3x { - font-size: 3em; -} - -.fa-4x { - font-size: 4em; -} - -.fa-5x { - font-size: 5em; -} - -.fa-6x { - font-size: 6em; -} - -.fa-7x { - font-size: 7em; -} - -.fa-8x { - font-size: 8em; -} - -.fa-9x { - font-size: 9em; -} - -.fa-10x { - font-size: 10em; -} - -.fa-2xs { - font-size: 0.625em; - line-height: 0.1em; - vertical-align: 0.225em; -} - -.fa-xs { - font-size: 0.75em; - line-height: 0.0833333337em; - vertical-align: 0.125em; -} - -.fa-sm { - font-size: 0.875em; - line-height: 0.0714285718em; - vertical-align: 0.0535714295em; -} - -.fa-lg { - font-size: 1.25em; - line-height: 0.05em; - vertical-align: -0.075em; -} - -.fa-xl { - font-size: 1.5em; - line-height: 0.0416666682em; - vertical-align: -0.125em; -} - -.fa-2xl { - font-size: 2em; - line-height: 0.03125em; - vertical-align: -0.1875em; -} - -.fa-fw { - text-align: center; - width: 1.25em; -} - -.fa-ul { - list-style-type: none; - margin-left: var(--fa-li-margin, 2.5em); - padding-left: 0; -} -.fa-ul > li { - position: relative; -} - -.fa-li { - left: calc(var(--fa-li-width, 2em) * -1); - position: absolute; - text-align: center; - width: var(--fa-li-width, 2em); - line-height: inherit; -} - -.fa-border { - border-color: var(--fa-border-color, #eee); - border-radius: var(--fa-border-radius, 0.1em); - border-style: var(--fa-border-style, solid); - border-width: var(--fa-border-width, 0.08em); - padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); -} - -.fa-pull-left { - float: left; - margin-right: var(--fa-pull-margin, 0.3em); -} - -.fa-pull-right { - float: right; - margin-left: var(--fa-pull-margin, 0.3em); -} - -.fa-beat { - -webkit-animation-name: fa-beat; - animation-name: fa-beat; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); - animation-timing-function: var(--fa-animation-timing, ease-in-out); -} - -.fa-bounce { - -webkit-animation-name: fa-bounce; - animation-name: fa-bounce; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); -} - -.fa-fade { - -webkit-animation-name: fa-fade; - animation-name: fa-fade; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); -} - -.fa-beat-fade { - -webkit-animation-name: fa-beat-fade; - animation-name: fa-beat-fade; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); -} - -.fa-flip { - -webkit-animation-name: fa-flip; - animation-name: fa-flip; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); - animation-timing-function: var(--fa-animation-timing, ease-in-out); -} - -.fa-shake { - -webkit-animation-name: fa-shake; - animation-name: fa-shake; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, linear); - animation-timing-function: var(--fa-animation-timing, linear); -} - -.fa-spin { - -webkit-animation-name: fa-spin; - animation-name: fa-spin; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 2s); - animation-duration: var(--fa-animation-duration, 2s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, linear); - animation-timing-function: var(--fa-animation-timing, linear); -} - -.fa-spin-reverse { - --fa-animation-direction: reverse; -} - -.fa-pulse, -.fa-spin-pulse { - -webkit-animation-name: fa-spin; - animation-name: fa-spin; - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); - animation-timing-function: var(--fa-animation-timing, steps(8)); -} - -@media (prefers-reduced-motion: reduce) { - .fa-beat, -.fa-bounce, -.fa-fade, -.fa-beat-fade, -.fa-flip, -.fa-pulse, -.fa-shake, -.fa-spin, -.fa-spin-pulse { - -webkit-animation-delay: -1ms; - animation-delay: -1ms; - -webkit-animation-duration: 1ms; - animation-duration: 1ms; - -webkit-animation-iteration-count: 1; - animation-iteration-count: 1; - -webkit-transition-delay: 0s; - transition-delay: 0s; - -webkit-transition-duration: 0s; - transition-duration: 0s; - } -} -@-webkit-keyframes fa-beat { - 0%, 90% { - -webkit-transform: scale(1); - transform: scale(1); - } - 45% { - -webkit-transform: scale(var(--fa-beat-scale, 1.25)); - transform: scale(var(--fa-beat-scale, 1.25)); - } -} -@keyframes fa-beat { - 0%, 90% { - -webkit-transform: scale(1); - transform: scale(1); - } - 45% { - -webkit-transform: scale(var(--fa-beat-scale, 1.25)); - transform: scale(var(--fa-beat-scale, 1.25)); - } -} -@-webkit-keyframes fa-bounce { - 0% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); - } - 10% { - -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - } - 30% { - -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - } - 50% { - -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - } - 57% { - -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - } - 64% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); - } - 100% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); - } -} -@keyframes fa-bounce { - 0% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); - } - 10% { - -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - } - 30% { - -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - } - 50% { - -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - } - 57% { - -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - } - 64% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); - } - 100% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); - } -} -@-webkit-keyframes fa-fade { - 50% { - opacity: var(--fa-fade-opacity, 0.4); - } -} -@keyframes fa-fade { - 50% { - opacity: var(--fa-fade-opacity, 0.4); - } -} -@-webkit-keyframes fa-beat-fade { - 0%, 100% { - opacity: var(--fa-beat-fade-opacity, 0.4); - -webkit-transform: scale(1); - transform: scale(1); - } - 50% { - opacity: 1; - -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); - transform: scale(var(--fa-beat-fade-scale, 1.125)); - } -} -@keyframes fa-beat-fade { - 0%, 100% { - opacity: var(--fa-beat-fade-opacity, 0.4); - -webkit-transform: scale(1); - transform: scale(1); - } - 50% { - opacity: 1; - -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); - transform: scale(var(--fa-beat-fade-scale, 1.125)); - } -} -@-webkit-keyframes fa-flip { - 50% { - -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - } -} -@keyframes fa-flip { - 50% { - -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - } -} -@-webkit-keyframes fa-shake { - 0% { - -webkit-transform: rotate(-15deg); - transform: rotate(-15deg); - } - 4% { - -webkit-transform: rotate(15deg); - transform: rotate(15deg); - } - 8%, 24% { - -webkit-transform: rotate(-18deg); - transform: rotate(-18deg); - } - 12%, 28% { - -webkit-transform: rotate(18deg); - transform: rotate(18deg); - } - 16% { - -webkit-transform: rotate(-22deg); - transform: rotate(-22deg); - } - 20% { - -webkit-transform: rotate(22deg); - transform: rotate(22deg); - } - 32% { - -webkit-transform: rotate(-12deg); - transform: rotate(-12deg); - } - 36% { - -webkit-transform: rotate(12deg); - transform: rotate(12deg); - } - 40%, 100% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } -} -@keyframes fa-shake { - 0% { - -webkit-transform: rotate(-15deg); - transform: rotate(-15deg); - } - 4% { - -webkit-transform: rotate(15deg); - transform: rotate(15deg); - } - 8%, 24% { - -webkit-transform: rotate(-18deg); - transform: rotate(-18deg); - } - 12%, 28% { - -webkit-transform: rotate(18deg); - transform: rotate(18deg); - } - 16% { - -webkit-transform: rotate(-22deg); - transform: rotate(-22deg); - } - 20% { - -webkit-transform: rotate(22deg); - transform: rotate(22deg); - } - 32% { - -webkit-transform: rotate(-12deg); - transform: rotate(-12deg); - } - 36% { - -webkit-transform: rotate(12deg); - transform: rotate(12deg); - } - 40%, 100% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } -} -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} -.fa-rotate-90 { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} - -.fa-rotate-180 { - -webkit-transform: rotate(180deg); - transform: rotate(180deg); -} - -.fa-rotate-270 { - -webkit-transform: rotate(270deg); - transform: rotate(270deg); -} - -.fa-flip-horizontal { - -webkit-transform: scale(-1, 1); - transform: scale(-1, 1); -} - -.fa-flip-vertical { - -webkit-transform: scale(1, -1); - transform: scale(1, -1); -} - -.fa-flip-both, -.fa-flip-horizontal.fa-flip-vertical { - -webkit-transform: scale(-1, -1); - transform: scale(-1, -1); -} - -.fa-rotate-by { - -webkit-transform: rotate(var(--fa-rotate-angle, none)); - transform: rotate(var(--fa-rotate-angle, none)); -} - -.fa-stack { - display: inline-block; - vertical-align: middle; - height: 2em; - position: relative; - width: 2.5em; -} - -.fa-stack-1x, -.fa-stack-2x { - bottom: 0; - left: 0; - margin: auto; - position: absolute; - right: 0; - top: 0; - z-index: var(--fa-stack-z-index, auto); -} - -.svg-inline--fa.fa-stack-1x { - height: 1em; - width: 1.25em; -} -.svg-inline--fa.fa-stack-2x { - height: 2em; - width: 2.5em; -} - -.fa-inverse { - color: var(--fa-inverse, #fff); -} - -.sr-only, -.fa-sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.sr-only-focusable:not(:focus), -.fa-sr-only-focusable:not(:focus) { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.svg-inline--fa .fa-primary { - fill: var(--fa-primary-color, currentColor); - opacity: var(--fa-primary-opacity, 1); -} - -.svg-inline--fa .fa-secondary { - fill: var(--fa-secondary-color, currentColor); - opacity: var(--fa-secondary-opacity, 0.4); -} - -.svg-inline--fa.fa-swap-opacity .fa-primary { - opacity: var(--fa-secondary-opacity, 0.4); -} - -.svg-inline--fa.fa-swap-opacity .fa-secondary { - opacity: var(--fa-primary-opacity, 1); -} - -.svg-inline--fa mask .fa-primary, -.svg-inline--fa mask .fa-secondary { - fill: black; -} - -.fad.fa-inverse, -.fa-duotone.fa-inverse { - color: var(--fa-inverse, #fff); -}`;function Th(){var e=vh,t=wh,n=C.cssPrefix,r=C.replacementClass,o=eg;if(n!==e||r!==t){var a=new RegExp("\\.".concat(e,"\\-"),"g"),i=new RegExp("\\--".concat(e,"\\-"),"g"),s=new RegExp("\\.".concat(t),"g");o=o.replace(a,".".concat(n,"-")).replace(i,"--".concat(n,"-")).replace(s,".".concat(r))}return o}var Du=!1;function ni(){C.autoAddCss&&!Du&&(qm(Th()),Du=!0)}var tg={mixout:function(){return{dom:{css:Th,insertCss:ni}}},hooks:function(){return{beforeDOMElementCreation:function(){ni()},beforeI2svg:function(){ni()}}}},st=Nt||{};st[it]||(st[it]={});st[it].styles||(st[it].styles={});st[it].hooks||(st[it].hooks={});st[it].shims||(st[it].shims=[]);var We=st[it],Eh=[],ng=function e(){U.removeEventListener("DOMContentLoaded",e),ra=1,Eh.map(function(t){return t()})},ra=!1;ct&&(ra=(U.documentElement.doScroll?/^loaded|^c/:/^loaded|^i|^c/).test(U.readyState),ra||U.addEventListener("DOMContentLoaded",ng));function rg(e){ct&&(ra?setTimeout(e,0):Eh.push(e))}function Hr(e){var t=e.tag,n=e.attributes,r=n===void 0?{}:n,o=e.children,a=o===void 0?[]:o;return typeof e=="string"?xh(e):"<".concat(t," ").concat(Qm(r),">").concat(a.map(Hr).join(""),"")}function Hu(e,t,n){if(e&&e[t]&&e[t][n])return{prefix:t,iconName:n,icon:e[t][n]}}var og=function(t,n){return function(r,o,a,i){return t.call(n,r,o,a,i)}},ri=function(t,n,r,o){var a=Object.keys(t),i=a.length,s=o!==void 0?og(n,o):n,l,u,d;for(r===void 0?(l=1,d=t[a[0]]):(l=0,d=r);l=55296&&o<=56319&&n=55296&&r<=56319&&n>t+1&&(o=e.charCodeAt(t+1),o>=56320&&o<=57343)?(r-55296)*1024+o-56320+65536:r}function Wu(e){return Object.keys(e).reduce(function(t,n){var r=e[n],o=!!r.icon;return o?t[r.iconName]=r.icon:t[n]=r,t},{})}function as(e,t){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},r=n.skipHooks,o=r===void 0?!1:r,a=Wu(t);typeof We.hooks.addPack=="function"&&!o?We.hooks.addPack(e,Wu(t)):We.styles[e]=S(S({},We.styles[e]||{}),a),e==="fas"&&as("fa",t)}var fo,po,mo,gn=We.styles,sg=We.shims,lg=(fo={},te(fo,W,Object.values(Nr[W])),te(fo,q,Object.values(Nr[q])),fo),gl=null,Ch={},Ph={},Ah={},Nh={},_h={},ug=(po={},te(po,W,Object.keys(Pr[W])),te(po,q,Object.keys(Pr[q])),po);function cg(e){return~Gm.indexOf(e)}function dg(e,t){var n=t.split("-"),r=n[0],o=n.slice(1).join("-");return r===e&&o!==""&&!cg(o)?o:null}var jh=function(){var t=function(a){return ri(gn,function(i,s,l){return i[l]=ri(s,a,{}),i},{})};Ch=t(function(o,a,i){if(a[3]&&(o[a[3]]=i),a[2]){var s=a[2].filter(function(l){return typeof l=="number"});s.forEach(function(l){o[l.toString(16)]=i})}return o}),Ph=t(function(o,a,i){if(o[i]=i,a[2]){var s=a[2].filter(function(l){return typeof l=="string"});s.forEach(function(l){o[l]=i})}return o}),_h=t(function(o,a,i){var s=a[2];return o[i]=i,s.forEach(function(l){o[l]=i}),o});var n="far"in gn||C.autoFetchSvg,r=ri(sg,function(o,a){var i=a[0],s=a[1],l=a[2];return s==="far"&&!n&&(s="fas"),typeof i=="string"&&(o.names[i]={prefix:s,iconName:l}),typeof i=="number"&&(o.unicodes[i.toString(16)]={prefix:s,iconName:l}),o},{names:{},unicodes:{}});Ah=r.names,Nh=r.unicodes,gl=xa(C.styleDefault,{family:C.familyDefault})};$m(function(e){gl=xa(e.styleDefault,{family:C.familyDefault})});jh();function yl(e,t){return(Ch[e]||{})[t]}function hg(e,t){return(Ph[e]||{})[t]}function Gt(e,t){return(_h[e]||{})[t]}function zh(e){return Ah[e]||{prefix:null,iconName:null}}function fg(e){var t=Nh[e],n=yl("fas",e);return t||(n?{prefix:"fas",iconName:n}:null)||{prefix:null,iconName:null}}function _t(){return gl}var vl=function(){return{prefix:null,iconName:null,rest:[]}};function xa(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=t.family,r=n===void 0?W:n,o=Pr[r][e],a=Ar[r][e]||Ar[r][o],i=e in We.styles?e:null;return a||i||null}var Vu=(mo={},te(mo,W,Object.keys(Nr[W])),te(mo,q,Object.keys(Nr[q])),mo);function Ta(e){var t,n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=n.skipLookups,o=r===void 0?!1:r,a=(t={},te(t,W,"".concat(C.cssPrefix,"-").concat(W)),te(t,q,"".concat(C.cssPrefix,"-").concat(q)),t),i=null,s=W;(e.includes(a[W])||e.some(function(u){return Vu[W].includes(u)}))&&(s=W),(e.includes(a[q])||e.some(function(u){return Vu[q].includes(u)}))&&(s=q);var l=e.reduce(function(u,d){var p=dg(C.cssPrefix,d);if(gn[d]?(d=lg[s].includes(d)?Lm[s][d]:d,i=d,u.prefix=d):ug[s].indexOf(d)>-1?(i=d,u.prefix=xa(d,{family:s})):p?u.iconName=p:d!==C.replacementClass&&d!==a[W]&&d!==a[q]&&u.rest.push(d),!o&&u.prefix&&u.iconName){var m=i==="fa"?zh(u.iconName):{},g=Gt(u.prefix,u.iconName);m.prefix&&(i=null),u.iconName=m.iconName||g||u.iconName,u.prefix=m.prefix||u.prefix,u.prefix==="far"&&!gn.far&&gn.fas&&!C.autoFetchSvg&&(u.prefix="fas")}return u},vl());return(e.includes("fa-brands")||e.includes("fab"))&&(l.prefix="fab"),(e.includes("fa-duotone")||e.includes("fad"))&&(l.prefix="fad"),!l.prefix&&s===q&&(gn.fass||C.autoFetchSvg)&&(l.prefix="fass",l.iconName=Gt(l.prefix,l.iconName)||l.iconName),(l.prefix==="fa"||i==="fa")&&(l.prefix=_t()||"fas"),l}var pg=function(){function e(){Em(this,e),this.definitions={}}return Cm(e,[{key:"add",value:function(){for(var n=this,r=arguments.length,o=new Array(r),a=0;a0&&d.forEach(function(p){typeof p=="string"&&(n[s][p]=u)}),n[s][l]=u}),n}}]),e}(),Uu=[],yn={},Tn={},mg=Object.keys(Tn);function gg(e,t){var n=t.mixoutsTo;return Uu=e,yn={},Object.keys(Tn).forEach(function(r){mg.indexOf(r)===-1&&delete Tn[r]}),Uu.forEach(function(r){var o=r.mixout?r.mixout():{};if(Object.keys(o).forEach(function(i){typeof o[i]=="function"&&(n[i]=o[i]),na(o[i])==="object"&&Object.keys(o[i]).forEach(function(s){n[i]||(n[i]={}),n[i][s]=o[i][s]})}),r.hooks){var a=r.hooks();Object.keys(a).forEach(function(i){yn[i]||(yn[i]=[]),yn[i].push(a[i])})}r.provides&&r.provides(Tn)}),n}function is(e,t){for(var n=arguments.length,r=new Array(n>2?n-2:0),o=2;o1?t-1:0),r=1;r0&&arguments[0]!==void 0?arguments[0]:{};return ct?(Xt("beforeI2svg",t),lt("pseudoElements2svg",t),lt("i2svg",t)):Promise.reject("Operation requires a DOM of some kind.")},watch:function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},n=t.autoReplaceSvgRoot;C.autoReplaceSvg===!1&&(C.autoReplaceSvg=!0),C.observeMutations=!0,rg(function(){bg({autoReplaceSvgRoot:n}),Xt("watch",t)})}},wg={icon:function(t){if(t===null)return null;if(na(t)==="object"&&t.prefix&&t.iconName)return{prefix:t.prefix,iconName:Gt(t.prefix,t.iconName)||t.iconName};if(Array.isArray(t)&&t.length===2){var n=t[1].indexOf("fa-")===0?t[1].slice(3):t[1],r=xa(t[0]);return{prefix:r,iconName:Gt(r,n)||n}}if(typeof t=="string"&&(t.indexOf("".concat(C.cssPrefix,"-"))>-1||t.match(Dm))){var o=Ta(t.split(" "),{skipLookups:!0});return{prefix:o.prefix||_t(),iconName:Gt(o.prefix,o.iconName)||o.iconName}}if(typeof t=="string"){var a=_t();return{prefix:a,iconName:Gt(a,t)||t}}}},Ae={noAuto:yg,config:C,dom:vg,parse:wg,library:Rh,findIconDefinition:ss,toHtml:Hr},bg=function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},n=t.autoReplaceSvgRoot,r=n===void 0?U:n;(Object.keys(We.styles).length>0||C.autoFetchSvg)&&ct&&C.autoReplaceSvg&&Ae.dom.i2svg({node:r})};function Ea(e,t){return Object.defineProperty(e,"abstract",{get:t}),Object.defineProperty(e,"html",{get:function(){return e.abstract.map(function(r){return Hr(r)})}}),Object.defineProperty(e,"node",{get:function(){if(ct){var r=U.createElement("div");return r.innerHTML=e.html,r.children}}}),e}function kg(e){var t=e.children,n=e.main,r=e.mask,o=e.attributes,a=e.styles,i=e.transform;if(ml(i)&&n.found&&!r.found){var s=n.width,l=n.height,u={x:s/l/2,y:.5};o.style=Ia(S(S({},a),{},{"transform-origin":"".concat(u.x+i.x/16,"em ").concat(u.y+i.y/16,"em")}))}return[{tag:"svg",attributes:o,children:t}]}function Sg(e){var t=e.prefix,n=e.iconName,r=e.children,o=e.attributes,a=e.symbol,i=a===!0?"".concat(t,"-").concat(C.cssPrefix,"-").concat(n):a;return[{tag:"svg",attributes:{style:"display: none;"},children:[{tag:"symbol",attributes:S(S({},o),{},{id:i}),children:r}]}]}function wl(e){var t=e.icons,n=t.main,r=t.mask,o=e.prefix,a=e.iconName,i=e.transform,s=e.symbol,l=e.title,u=e.maskId,d=e.titleId,p=e.extra,m=e.watchable,g=m===void 0?!1:m,v=r.found?r:n,b=v.width,N=v.height,h=o==="fak",c=[C.replacementClass,a?"".concat(C.cssPrefix,"-").concat(a):""].filter(function(F){return p.classes.indexOf(F)===-1}).filter(function(F){return F!==""||!!F}).concat(p.classes).join(" "),f={children:[],attributes:S(S({},p.attributes),{},{"data-prefix":o,"data-icon":a,class:c,role:p.attributes.role||"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 ".concat(b," ").concat(N)})},y=h&&!~p.classes.indexOf("fa-fw")?{width:"".concat(b/N*16*.0625,"em")}:{};g&&(f.attributes[Kt]=""),l&&(f.children.push({tag:"title",attributes:{id:f.attributes["aria-labelledby"]||"title-".concat(d||jr())},children:[l]}),delete f.attributes.title);var k=S(S({},f),{},{prefix:o,iconName:a,main:n,mask:r,maskId:u,transform:i,symbol:s,styles:S(S({},y),p.styles)}),x=r.found&&n.found?lt("generateAbstractMask",k)||{children:[],attributes:{}}:lt("generateAbstractIcon",k)||{children:[],attributes:{}},T=x.children,P=x.attributes;return k.children=T,k.attributes=P,s?Sg(k):kg(k)}function Gu(e){var t=e.content,n=e.width,r=e.height,o=e.transform,a=e.title,i=e.extra,s=e.watchable,l=s===void 0?!1:s,u=S(S(S({},i.attributes),a?{title:a}:{}),{},{class:i.classes.join(" ")});l&&(u[Kt]="");var d=S({},i.styles);ml(o)&&(d.transform=Xm({transform:o,startCentered:!0,width:n,height:r}),d["-webkit-transform"]=d.transform);var p=Ia(d);p.length>0&&(u.style=p);var m=[];return m.push({tag:"span",attributes:u,children:[t]}),a&&m.push({tag:"span",attributes:{class:"sr-only"},children:[a]}),m}function Ig(e){var t=e.content,n=e.title,r=e.extra,o=S(S(S({},r.attributes),n?{title:n}:{}),{},{class:r.classes.join(" ")}),a=Ia(r.styles);a.length>0&&(o.style=a);var i=[];return i.push({tag:"span",attributes:o,children:[t]}),n&&i.push({tag:"span",attributes:{class:"sr-only"},children:[n]}),i}var oi=We.styles;function ls(e){var t=e[0],n=e[1],r=e.slice(4),o=ul(r,1),a=o[0],i=null;return Array.isArray(a)?i={tag:"g",attributes:{class:"".concat(C.cssPrefix,"-").concat(Ut.GROUP)},children:[{tag:"path",attributes:{class:"".concat(C.cssPrefix,"-").concat(Ut.SECONDARY),fill:"currentColor",d:a[0]}},{tag:"path",attributes:{class:"".concat(C.cssPrefix,"-").concat(Ut.PRIMARY),fill:"currentColor",d:a[1]}}]}:i={tag:"path",attributes:{fill:"currentColor",d:a}},{found:!0,width:t,height:n,icon:i}}var xg={found:!1,width:512,height:512};function Tg(e,t){!bh&&!C.showMissingIcons&&e&&console.error('Icon with name "'.concat(e,'" and prefix "').concat(t,'" is missing.'))}function us(e,t){var n=t;return t==="fa"&&C.styleDefault!==null&&(t=_t()),new Promise(function(r,o){if(lt("missingIconAbstract"),n==="fa"){var a=zh(e)||{};e=a.iconName||e,t=a.prefix||t}if(e&&t&&oi[t]&&oi[t][e]){var i=oi[t][e];return r(ls(i))}Tg(e,t),r(S(S({},xg),{},{icon:C.showMissingIcons&&e?lt("missingIconAbstract")||{}:{}}))})}var Bu=function(){},cs=C.measurePerformance&&io&&io.mark&&io.measure?io:{mark:Bu,measure:Bu},Xn='FA "6.3.0"',Eg=function(t){return cs.mark("".concat(Xn," ").concat(t," begins")),function(){return Oh(t)}},Oh=function(t){cs.mark("".concat(Xn," ").concat(t," ends")),cs.measure("".concat(Xn," ").concat(t),"".concat(Xn," ").concat(t," begins"),"".concat(Xn," ").concat(t," ends"))},bl={begin:Eg,end:Oh},Po=function(){};function Yu(e){var t=e.getAttribute?e.getAttribute(Kt):null;return typeof t=="string"}function Cg(e){var t=e.getAttribute?e.getAttribute(dl):null,n=e.getAttribute?e.getAttribute(hl):null;return t&&n}function Pg(e){return e&&e.classList&&e.classList.contains&&e.classList.contains(C.replacementClass)}function Ag(){if(C.autoReplaceSvg===!0)return Ao.replace;var e=Ao[C.autoReplaceSvg];return e||Ao.replace}function Ng(e){return U.createElementNS("http://www.w3.org/2000/svg",e)}function _g(e){return U.createElement(e)}function Mh(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=t.ceFn,r=n===void 0?e.tag==="svg"?Ng:_g:n;if(typeof e=="string")return U.createTextNode(e);var o=r(e.tag);Object.keys(e.attributes||[]).forEach(function(i){o.setAttribute(i,e.attributes[i])});var a=e.children||[];return a.forEach(function(i){o.appendChild(Mh(i,{ceFn:r}))}),o}function jg(e){var t=" ".concat(e.outerHTML," ");return t="".concat(t,"Font Awesome fontawesome.com "),t}var Ao={replace:function(t){var n=t[0];if(n.parentNode)if(t[1].forEach(function(o){n.parentNode.insertBefore(Mh(o),n)}),n.getAttribute(Kt)===null&&C.keepOriginalSource){var r=U.createComment(jg(n));n.parentNode.replaceChild(r,n)}else n.remove()},nest:function(t){var n=t[0],r=t[1];if(~pl(n).indexOf(C.replacementClass))return Ao.replace(t);var o=new RegExp("".concat(C.cssPrefix,"-.*"));if(delete r[0].attributes.id,r[0].attributes.class){var a=r[0].attributes.class.split(" ").reduce(function(s,l){return l===C.replacementClass||l.match(o)?s.toSvg.push(l):s.toNode.push(l),s},{toNode:[],toSvg:[]});r[0].attributes.class=a.toSvg.join(" "),a.toNode.length===0?n.removeAttribute("class"):n.setAttribute("class",a.toNode.join(" "))}var i=r.map(function(s){return Hr(s)}).join(` -`);n.setAttribute(Kt,""),n.innerHTML=i}};function Ju(e){e()}function Fh(e,t){var n=typeof t=="function"?t:Po;if(e.length===0)n();else{var r=Ju;C.mutateApproach===Mm&&(r=Nt.requestAnimationFrame||Ju),r(function(){var o=Ag(),a=bl.begin("mutate");e.map(o),a(),n()})}}var kl=!1;function Lh(){kl=!0}function ds(){kl=!1}var oa=null;function $u(e){if(Fu&&C.observeMutations){var t=e.treeCallback,n=t===void 0?Po:t,r=e.nodeCallback,o=r===void 0?Po:r,a=e.pseudoElementsCallback,i=a===void 0?Po:a,s=e.observeMutationsRoot,l=s===void 0?U:s;oa=new Fu(function(u){if(!kl){var d=_t();Fn(u).forEach(function(p){if(p.type==="childList"&&p.addedNodes.length>0&&!Yu(p.addedNodes[0])&&(C.searchPseudoElements&&i(p.target),n(p.target)),p.type==="attributes"&&p.target.parentNode&&C.searchPseudoElements&&i(p.target.parentNode),p.type==="attributes"&&Yu(p.target)&&~Um.indexOf(p.attributeName))if(p.attributeName==="class"&&Cg(p.target)){var m=Ta(pl(p.target)),g=m.prefix,v=m.iconName;p.target.setAttribute(dl,g||d),v&&p.target.setAttribute(hl,v)}else Pg(p.target)&&o(p.target)})}}),ct&&oa.observe(l,{childList:!0,attributes:!0,characterData:!0,subtree:!0})}}function zg(){oa&&oa.disconnect()}function Rg(e){var t=e.getAttribute("style"),n=[];return t&&(n=t.split(";").reduce(function(r,o){var a=o.split(":"),i=a[0],s=a.slice(1);return i&&s.length>0&&(r[i]=s.join(":").trim()),r},{})),n}function Og(e){var t=e.getAttribute("data-prefix"),n=e.getAttribute("data-icon"),r=e.innerText!==void 0?e.innerText.trim():"",o=Ta(pl(e));return o.prefix||(o.prefix=_t()),t&&n&&(o.prefix=t,o.iconName=n),o.iconName&&o.prefix||(o.prefix&&r.length>0&&(o.iconName=hg(o.prefix,e.innerText)||yl(o.prefix,os(e.innerText))),!o.iconName&&C.autoFetchSvg&&e.firstChild&&e.firstChild.nodeType===Node.TEXT_NODE&&(o.iconName=e.firstChild.data)),o}function Mg(e){var t=Fn(e.attributes).reduce(function(o,a){return o.name!=="class"&&o.name!=="style"&&(o[a.name]=a.value),o},{}),n=e.getAttribute("title"),r=e.getAttribute("data-fa-title-id");return C.autoA11y&&(n?t["aria-labelledby"]="".concat(C.replacementClass,"-title-").concat(r||jr()):(t["aria-hidden"]="true",t.focusable="false")),t}function Fg(){return{iconName:null,title:null,titleId:null,prefix:null,transform:$e,symbol:!1,mask:{iconName:null,prefix:null,rest:[]},maskId:null,extra:{classes:[],styles:{},attributes:{}}}}function qu(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{styleParser:!0},n=Og(e),r=n.iconName,o=n.prefix,a=n.rest,i=Mg(e),s=is("parseNodeAttributes",{},e),l=t.styleParser?Rg(e):[];return S({iconName:r,title:e.getAttribute("title"),titleId:e.getAttribute("data-fa-title-id"),prefix:o,transform:$e,mask:{iconName:null,prefix:null,rest:[]},maskId:null,symbol:!1,extra:{classes:a,styles:l,attributes:i}},s)}var Lg=We.styles;function Dh(e){var t=C.autoReplaceSvg==="nest"?qu(e,{styleParser:!1}):qu(e);return~t.extra.classes.indexOf(kh)?lt("generateLayersText",e,t):lt("generateSvgReplacementMutation",e,t)}var jt=new Set;fl.map(function(e){jt.add("fa-".concat(e))});Object.keys(Pr[W]).map(jt.add.bind(jt));Object.keys(Pr[q]).map(jt.add.bind(jt));jt=Lr(jt);function Zu(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;if(!ct)return Promise.resolve();var n=U.documentElement.classList,r=function(p){return n.add("".concat(Lu,"-").concat(p))},o=function(p){return n.remove("".concat(Lu,"-").concat(p))},a=C.autoFetchSvg?jt:fl.map(function(d){return"fa-".concat(d)}).concat(Object.keys(Lg));a.includes("fa")||a.push("fa");var i=[".".concat(kh,":not([").concat(Kt,"])")].concat(a.map(function(d){return".".concat(d,":not([").concat(Kt,"])")})).join(", ");if(i.length===0)return Promise.resolve();var s=[];try{s=Fn(e.querySelectorAll(i))}catch{}if(s.length>0)r("pending"),o("complete");else return Promise.resolve();var l=bl.begin("onTree"),u=s.reduce(function(d,p){try{var m=Dh(p);m&&d.push(m)}catch(g){bh||g.name==="MissingIcon"&&console.error(g)}return d},[]);return new Promise(function(d,p){Promise.all(u).then(function(m){Fh(m,function(){r("active"),r("complete"),o("pending"),typeof t=="function"&&t(),l(),d()})}).catch(function(m){l(),p(m)})})}function Dg(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;Dh(e).then(function(n){n&&Fh([n],t)})}function Hg(e){return function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=(t||{}).icon?t:ss(t||{}),o=n.mask;return o&&(o=(o||{}).icon?o:ss(o||{})),e(r,S(S({},n),{},{mask:o}))}}var Wg=function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=n.transform,o=r===void 0?$e:r,a=n.symbol,i=a===void 0?!1:a,s=n.mask,l=s===void 0?null:s,u=n.maskId,d=u===void 0?null:u,p=n.title,m=p===void 0?null:p,g=n.titleId,v=g===void 0?null:g,b=n.classes,N=b===void 0?[]:b,h=n.attributes,c=h===void 0?{}:h,f=n.styles,y=f===void 0?{}:f;if(t){var k=t.prefix,x=t.iconName,T=t.icon;return Ea(S({type:"icon"},t),function(){return Xt("beforeDOMElementCreation",{iconDefinition:t,params:n}),C.autoA11y&&(m?c["aria-labelledby"]="".concat(C.replacementClass,"-title-").concat(v||jr()):(c["aria-hidden"]="true",c.focusable="false")),wl({icons:{main:ls(T),mask:l?ls(l.icon):{found:!1,width:null,height:null,icon:{}}},prefix:k,iconName:x,transform:S(S({},$e),o),symbol:i,title:m,maskId:d,titleId:v,extra:{attributes:c,styles:y,classes:N}})})}},Vg={mixout:function(){return{icon:Hg(Wg)}},hooks:function(){return{mutationObserverCallbacks:function(n){return n.treeCallback=Zu,n.nodeCallback=Dg,n}}},provides:function(t){t.i2svg=function(n){var r=n.node,o=r===void 0?U:r,a=n.callback,i=a===void 0?function(){}:a;return Zu(o,i)},t.generateSvgReplacementMutation=function(n,r){var o=r.iconName,a=r.title,i=r.titleId,s=r.prefix,l=r.transform,u=r.symbol,d=r.mask,p=r.maskId,m=r.extra;return new Promise(function(g,v){Promise.all([us(o,s),d.iconName?us(d.iconName,d.prefix):Promise.resolve({found:!1,width:512,height:512,icon:{}})]).then(function(b){var N=ul(b,2),h=N[0],c=N[1];g([n,wl({icons:{main:h,mask:c},prefix:s,iconName:o,transform:l,symbol:u,maskId:p,title:a,titleId:i,extra:m,watchable:!0})])}).catch(v)})},t.generateAbstractIcon=function(n){var r=n.children,o=n.attributes,a=n.main,i=n.transform,s=n.styles,l=Ia(s);l.length>0&&(o.style=l);var u;return ml(i)&&(u=lt("generateAbstractTransformGrouping",{main:a,transform:i,containerWidth:a.width,iconWidth:a.width})),r.push(u||a.icon),{children:r,attributes:o}}}},Ug={mixout:function(){return{layer:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},o=r.classes,a=o===void 0?[]:o;return Ea({type:"layer"},function(){Xt("beforeDOMElementCreation",{assembler:n,params:r});var i=[];return n(function(s){Array.isArray(s)?s.map(function(l){i=i.concat(l.abstract)}):i=i.concat(s.abstract)}),[{tag:"span",attributes:{class:["".concat(C.cssPrefix,"-layers")].concat(Lr(a)).join(" ")},children:i}]})}}}},Gg={mixout:function(){return{counter:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},o=r.title,a=o===void 0?null:o,i=r.classes,s=i===void 0?[]:i,l=r.attributes,u=l===void 0?{}:l,d=r.styles,p=d===void 0?{}:d;return Ea({type:"counter",content:n},function(){return Xt("beforeDOMElementCreation",{content:n,params:r}),Ig({content:n.toString(),title:a,extra:{attributes:u,styles:p,classes:["".concat(C.cssPrefix,"-layers-counter")].concat(Lr(s))}})})}}}},Bg={mixout:function(){return{text:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},o=r.transform,a=o===void 0?$e:o,i=r.title,s=i===void 0?null:i,l=r.classes,u=l===void 0?[]:l,d=r.attributes,p=d===void 0?{}:d,m=r.styles,g=m===void 0?{}:m;return Ea({type:"text",content:n},function(){return Xt("beforeDOMElementCreation",{content:n,params:r}),Gu({content:n,transform:S(S({},$e),a),title:s,extra:{attributes:p,styles:g,classes:["".concat(C.cssPrefix,"-layers-text")].concat(Lr(u))}})})}}},provides:function(t){t.generateLayersText=function(n,r){var o=r.title,a=r.transform,i=r.extra,s=null,l=null;if(yh){var u=parseInt(getComputedStyle(n).fontSize,10),d=n.getBoundingClientRect();s=d.width/u,l=d.height/u}return C.autoA11y&&!o&&(i.attributes["aria-hidden"]="true"),Promise.resolve([n,Gu({content:n.innerHTML,width:s,height:l,transform:a,title:o,extra:i,watchable:!0})])}}},Yg=new RegExp('"',"ug"),Qu=[1105920,1112319];function Jg(e){var t=e.replace(Yg,""),n=ig(t,0),r=n>=Qu[0]&&n<=Qu[1],o=t.length===2?t[0]===t[1]:!1;return{value:os(o?t[0]:t),isSecondary:r||o}}function Ku(e,t){var n="".concat(Om).concat(t.replace(":","-"));return new Promise(function(r,o){if(e.getAttribute(n)!==null)return r();var a=Fn(e.children),i=a.filter(function(T){return T.getAttribute(rs)===t})[0],s=Nt.getComputedStyle(e,t),l=s.getPropertyValue("font-family").match(Hm),u=s.getPropertyValue("font-weight"),d=s.getPropertyValue("content");if(i&&!l)return e.removeChild(i),r();if(l&&d!=="none"&&d!==""){var p=s.getPropertyValue("content"),m=~["Sharp"].indexOf(l[2])?q:W,g=~["Solid","Regular","Light","Thin","Duotone","Brands","Kit"].indexOf(l[2])?Ar[m][l[2].toLowerCase()]:Wm[m][u],v=Jg(p),b=v.value,N=v.isSecondary,h=l[0].startsWith("FontAwesome"),c=yl(g,b),f=c;if(h){var y=fg(b);y.iconName&&y.prefix&&(c=y.iconName,g=y.prefix)}if(c&&!N&&(!i||i.getAttribute(dl)!==g||i.getAttribute(hl)!==f)){e.setAttribute(n,f),i&&e.removeChild(i);var k=Fg(),x=k.extra;x.attributes[rs]=t,us(c,g).then(function(T){var P=wl(S(S({},k),{},{icons:{main:T,mask:vl()},prefix:g,iconName:f,extra:x,watchable:!0})),F=U.createElement("svg");t==="::before"?e.insertBefore(F,e.firstChild):e.appendChild(F),F.outerHTML=P.map(function(R){return Hr(R)}).join(` -`),e.removeAttribute(n),r()}).catch(o)}else r()}else r()})}function $g(e){return Promise.all([Ku(e,"::before"),Ku(e,"::after")])}function qg(e){return e.parentNode!==document.head&&!~Fm.indexOf(e.tagName.toUpperCase())&&!e.getAttribute(rs)&&(!e.parentNode||e.parentNode.tagName!=="svg")}function Xu(e){if(ct)return new Promise(function(t,n){var r=Fn(e.querySelectorAll("*")).filter(qg).map($g),o=bl.begin("searchPseudoElements");Lh(),Promise.all(r).then(function(){o(),ds(),t()}).catch(function(){o(),ds(),n()})})}var Zg={hooks:function(){return{mutationObserverCallbacks:function(n){return n.pseudoElementsCallback=Xu,n}}},provides:function(t){t.pseudoElements2svg=function(n){var r=n.node,o=r===void 0?U:r;C.searchPseudoElements&&Xu(o)}}},ec=!1,Qg={mixout:function(){return{dom:{unwatch:function(){Lh(),ec=!0}}}},hooks:function(){return{bootstrap:function(){$u(is("mutationObserverCallbacks",{}))},noAuto:function(){zg()},watch:function(n){var r=n.observeMutationsRoot;ec?ds():$u(is("mutationObserverCallbacks",{observeMutationsRoot:r}))}}}},tc=function(t){var n={size:16,x:0,y:0,flipX:!1,flipY:!1,rotate:0};return t.toLowerCase().split(" ").reduce(function(r,o){var a=o.toLowerCase().split("-"),i=a[0],s=a.slice(1).join("-");if(i&&s==="h")return r.flipX=!0,r;if(i&&s==="v")return r.flipY=!0,r;if(s=parseFloat(s),isNaN(s))return r;switch(i){case"grow":r.size=r.size+s;break;case"shrink":r.size=r.size-s;break;case"left":r.x=r.x-s;break;case"right":r.x=r.x+s;break;case"up":r.y=r.y-s;break;case"down":r.y=r.y+s;break;case"rotate":r.rotate=r.rotate+s;break}return r},n)},Kg={mixout:function(){return{parse:{transform:function(n){return tc(n)}}}},hooks:function(){return{parseNodeAttributes:function(n,r){var o=r.getAttribute("data-fa-transform");return o&&(n.transform=tc(o)),n}}},provides:function(t){t.generateAbstractTransformGrouping=function(n){var r=n.main,o=n.transform,a=n.containerWidth,i=n.iconWidth,s={transform:"translate(".concat(a/2," 256)")},l="translate(".concat(o.x*32,", ").concat(o.y*32,") "),u="scale(".concat(o.size/16*(o.flipX?-1:1),", ").concat(o.size/16*(o.flipY?-1:1),") "),d="rotate(".concat(o.rotate," 0 0)"),p={transform:"".concat(l," ").concat(u," ").concat(d)},m={transform:"translate(".concat(i/2*-1," -256)")},g={outer:s,inner:p,path:m};return{tag:"g",attributes:S({},g.outer),children:[{tag:"g",attributes:S({},g.inner),children:[{tag:r.icon.tag,children:r.icon.children,attributes:S(S({},r.icon.attributes),g.path)}]}]}}}},ai={x:0,y:0,width:"100%",height:"100%"};function nc(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return e.attributes&&(e.attributes.fill||t)&&(e.attributes.fill="black"),e}function Xg(e){return e.tag==="g"?e.children:[e]}var e1={hooks:function(){return{parseNodeAttributes:function(n,r){var o=r.getAttribute("data-fa-mask"),a=o?Ta(o.split(" ").map(function(i){return i.trim()})):vl();return a.prefix||(a.prefix=_t()),n.mask=a,n.maskId=r.getAttribute("data-fa-mask-id"),n}}},provides:function(t){t.generateAbstractMask=function(n){var r=n.children,o=n.attributes,a=n.main,i=n.mask,s=n.maskId,l=n.transform,u=a.width,d=a.icon,p=i.width,m=i.icon,g=Km({transform:l,containerWidth:p,iconWidth:u}),v={tag:"rect",attributes:S(S({},ai),{},{fill:"white"})},b=d.children?{children:d.children.map(nc)}:{},N={tag:"g",attributes:S({},g.inner),children:[nc(S({tag:d.tag,attributes:S(S({},d.attributes),g.path)},b))]},h={tag:"g",attributes:S({},g.outer),children:[N]},c="mask-".concat(s||jr()),f="clip-".concat(s||jr()),y={tag:"mask",attributes:S(S({},ai),{},{id:c,maskUnits:"userSpaceOnUse",maskContentUnits:"userSpaceOnUse"}),children:[v,h]},k={tag:"defs",children:[{tag:"clipPath",attributes:{id:f},children:Xg(m)},y]};return r.push(k,{tag:"rect",attributes:S({fill:"currentColor","clip-path":"url(#".concat(f,")"),mask:"url(#".concat(c,")")},ai)}),{children:r,attributes:o}}}},t1={provides:function(t){var n=!1;Nt.matchMedia&&(n=Nt.matchMedia("(prefers-reduced-motion: reduce)").matches),t.missingIconAbstract=function(){var r=[],o={fill:"currentColor"},a={attributeType:"XML",repeatCount:"indefinite",dur:"2s"};r.push({tag:"path",attributes:S(S({},o),{},{d:"M156.5,447.7l-12.6,29.5c-18.7-9.5-35.9-21.2-51.5-34.9l22.7-22.7C127.6,430.5,141.5,440,156.5,447.7z M40.6,272H8.5 c1.4,21.2,5.4,41.7,11.7,61.1L50,321.2C45.1,305.5,41.8,289,40.6,272z M40.6,240c1.4-18.8,5.2-37,11.1-54.1l-29.5-12.6 C14.7,194.3,10,216.7,8.5,240H40.6z M64.3,156.5c7.8-14.9,17.2-28.8,28.1-41.5L69.7,92.3c-13.7,15.6-25.5,32.8-34.9,51.5 L64.3,156.5z M397,419.6c-13.9,12-29.4,22.3-46.1,30.4l11.9,29.8c20.7-9.9,39.8-22.6,56.9-37.6L397,419.6z M115,92.4 c13.9-12,29.4-22.3,46.1-30.4l-11.9-29.8c-20.7,9.9-39.8,22.6-56.8,37.6L115,92.4z M447.7,355.5c-7.8,14.9-17.2,28.8-28.1,41.5 l22.7,22.7c13.7-15.6,25.5-32.9,34.9-51.5L447.7,355.5z M471.4,272c-1.4,18.8-5.2,37-11.1,54.1l29.5,12.6 c7.5-21.1,12.2-43.5,13.6-66.8H471.4z M321.2,462c-15.7,5-32.2,8.2-49.2,9.4v32.1c21.2-1.4,41.7-5.4,61.1-11.7L321.2,462z M240,471.4c-18.8-1.4-37-5.2-54.1-11.1l-12.6,29.5c21.1,7.5,43.5,12.2,66.8,13.6V471.4z M462,190.8c5,15.7,8.2,32.2,9.4,49.2h32.1 c-1.4-21.2-5.4-41.7-11.7-61.1L462,190.8z M92.4,397c-12-13.9-22.3-29.4-30.4-46.1l-29.8,11.9c9.9,20.7,22.6,39.8,37.6,56.9 L92.4,397z M272,40.6c18.8,1.4,36.9,5.2,54.1,11.1l12.6-29.5C317.7,14.7,295.3,10,272,8.5V40.6z M190.8,50 c15.7-5,32.2-8.2,49.2-9.4V8.5c-21.2,1.4-41.7,5.4-61.1,11.7L190.8,50z M442.3,92.3L419.6,115c12,13.9,22.3,29.4,30.5,46.1 l29.8-11.9C470,128.5,457.3,109.4,442.3,92.3z M397,92.4l22.7-22.7c-15.6-13.7-32.8-25.5-51.5-34.9l-12.6,29.5 C370.4,72.1,384.4,81.5,397,92.4z"})});var i=S(S({},a),{},{attributeName:"opacity"}),s={tag:"circle",attributes:S(S({},o),{},{cx:"256",cy:"364",r:"28"}),children:[]};return n||s.children.push({tag:"animate",attributes:S(S({},a),{},{attributeName:"r",values:"28;14;28;28;14;28;"})},{tag:"animate",attributes:S(S({},i),{},{values:"1;0;1;1;0;1;"})}),r.push(s),r.push({tag:"path",attributes:S(S({},o),{},{opacity:"1",d:"M263.7,312h-16c-6.6,0-12-5.4-12-12c0-71,77.4-63.9,77.4-107.8c0-20-17.8-40.2-57.4-40.2c-29.1,0-44.3,9.6-59.2,28.7 c-3.9,5-11.1,6-16.2,2.4l-13.1-9.2c-5.6-3.9-6.9-11.8-2.6-17.2c21.2-27.2,46.4-44.7,91.2-44.7c52.3,0,97.4,29.8,97.4,80.2 c0,67.6-77.4,63.5-77.4,107.8C275.7,306.6,270.3,312,263.7,312z"}),children:n?[]:[{tag:"animate",attributes:S(S({},i),{},{values:"1;0;0;0;0;1;"})}]}),n||r.push({tag:"path",attributes:S(S({},o),{},{opacity:"0",d:"M232.5,134.5l7,168c0.3,6.4,5.6,11.5,12,11.5h9c6.4,0,11.7-5.1,12-11.5l7-168c0.3-6.8-5.2-12.5-12-12.5h-23 C237.7,122,232.2,127.7,232.5,134.5z"}),children:[{tag:"animate",attributes:S(S({},i),{},{values:"0;0;1;1;0;0;"})}]}),{tag:"g",attributes:{class:"missing"},children:r}}}},n1={hooks:function(){return{parseNodeAttributes:function(n,r){var o=r.getAttribute("data-fa-symbol"),a=o===null?!1:o===""?!0:o;return n.symbol=a,n}}}},r1=[tg,Vg,Ug,Gg,Bg,Zg,Qg,Kg,e1,t1,n1];gg(r1,{mixoutsTo:Ae});Ae.noAuto;Ae.config;Ae.library;Ae.dom;var hs=Ae.parse;Ae.findIconDefinition;Ae.toHtml;var o1=Ae.icon;Ae.layer;Ae.text;Ae.counter;var j={},a1={get exports(){return j},set exports(e){j=e}},i1="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED",s1=i1,l1=s1;function Hh(){}function Wh(){}Wh.resetWarningCache=Hh;var u1=function(){function e(r,o,a,i,s,l){if(l!==l1){var u=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw u.name="Invariant Violation",u}}e.isRequired=e;function t(){return e}var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:Wh,resetWarningCache:Hh};return n.PropTypes=n,n};a1.exports=u1();function rc(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function wt(e){for(var t=1;t=0)&&(n[o]=e[o]);return n}function d1(e,t){if(e==null)return{};var n=c1(e,t),r,o;if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function fs(e){return h1(e)||f1(e)||p1(e)||m1()}function h1(e){if(Array.isArray(e))return ps(e)}function f1(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function p1(e,t){if(e){if(typeof e=="string")return ps(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return ps(e,t)}}function ps(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&arguments[2]!==void 0?arguments[2]:{};if(typeof t=="string")return t;var r=(t.children||[]).map(function(l){return Uh(e,l)}),o=Object.keys(t.attributes||{}).reduce(function(l,u){var d=t.attributes[u];switch(u){case"class":l.attrs.className=d,delete t.attributes.class;break;case"style":l.attrs.style=b1(d);break;default:u.indexOf("aria-")===0||u.indexOf("data-")===0?l.attrs[u.toLowerCase()]=d:l.attrs[Vh(u)]=d}return l},{attrs:{}}),a=n.style,i=a===void 0?{}:a,s=d1(n,v1);return o.attrs.style=wt(wt({},o.attrs.style),i),e.apply(void 0,[t.tag,wt(wt({},o.attrs),s)].concat(fs(r)))}var Gh=!1;try{Gh=!0}catch{}function k1(){if(!Gh&&console&&typeof console.error=="function"){var e;(e=console).error.apply(e,arguments)}}function oc(e){if(e&&aa(e)==="object"&&e.prefix&&e.iconName&&e.icon)return e;if(hs.icon)return hs.icon(e);if(e===null)return null;if(e&&aa(e)==="object"&&e.prefix&&e.iconName)return e;if(Array.isArray(e)&&e.length===2)return{prefix:e[0],iconName:e[1]};if(typeof e=="string")return{prefix:"fas",iconName:e}}function ii(e,t){return Array.isArray(t)&&t.length>0||!Array.isArray(t)&&t?vn({},e,t):{}}var Ln=sa.forwardRef(function(e,t){var n=e.icon,r=e.mask,o=e.symbol,a=e.className,i=e.title,s=e.titleId,l=e.maskId,u=oc(n),d=ii("classes",[].concat(fs(g1(e)),fs(a.split(" ")))),p=ii("transform",typeof e.transform=="string"?hs.transform(e.transform):e.transform),m=ii("mask",oc(r)),g=o1(u,wt(wt(wt(wt({},d),p),m),{},{symbol:o,title:i,titleId:s,maskId:l}));if(!g)return k1("Could not find icon",u),null;var v=g.abstract,b={ref:t};return Object.keys(e).forEach(function(N){Ln.defaultProps.hasOwnProperty(N)||(b[N]=e[N])}),S1(v[0],b)});Ln.displayName="FontAwesomeIcon";Ln.propTypes={beat:j.bool,border:j.bool,beatFade:j.bool,bounce:j.bool,className:j.string,fade:j.bool,flash:j.bool,mask:j.oneOfType([j.object,j.array,j.string]),maskId:j.string,fixedWidth:j.bool,inverse:j.bool,flip:j.oneOf([!0,!1,"horizontal","vertical","both"]),icon:j.oneOfType([j.object,j.array,j.string]),listItem:j.bool,pull:j.oneOf(["right","left"]),pulse:j.bool,rotation:j.oneOf([0,90,180,270]),shake:j.bool,size:j.oneOf(["2xs","xs","sm","lg","xl","2xl","1x","2x","3x","4x","5x","6x","7x","8x","9x","10x"]),spin:j.bool,spinPulse:j.bool,spinReverse:j.bool,symbol:j.oneOfType([j.bool,j.string]),title:j.string,titleId:j.string,transform:j.oneOfType([j.string,j.object]),swapOpacity:j.bool};Ln.defaultProps={border:!1,className:"",mask:null,maskId:null,fixedWidth:!1,inverse:!1,flip:!1,icon:null,listItem:!1,pull:null,pulse:!1,rotation:null,size:null,spin:!1,spinPulse:!1,spinReverse:!1,beat:!1,fade:!1,beatFade:!1,bounce:!1,shake:!1,symbol:!1,title:"",titleId:null,transform:null,swapOpacity:!1};var S1=Uh.bind(null,sa.createElement),I1={prefix:"fab",iconName:"css3-alt",icon:[384,512,[],"f38b","M0 32l34.9 395.8L192 480l157.1-52.2L384 32H0zm313.1 80l-4.8 47.3L193 208.6l-.3.1h111.5l-12.8 146.6-98.2 28.7-98.8-29.2-6.4-73.9h48.9l3.2 38.3 52.6 13.3 54.7-15.4 3.7-61.6-166.3-.5v-.1l-.2.1-3.6-46.3L193.1 162l6.5-2.7H76.7L70.9 112h242.2z"]},x1={prefix:"fab",iconName:"markdown",icon:[640,512,[],"f60f","M593.8 59.1H46.2C20.7 59.1 0 79.8 0 105.2v301.5c0 25.5 20.7 46.2 46.2 46.2h547.7c25.5 0 46.2-20.7 46.1-46.1V105.2c0-25.4-20.7-46.1-46.2-46.1zM338.5 360.6H277v-120l-61.5 76.9-61.5-76.9v120H92.3V151.4h61.5l61.5 76.9 61.5-76.9h61.5v209.2zm135.3 3.1L381.5 256H443V151.4h61.5V256H566z"]},T1={prefix:"fab",iconName:"node",icon:[640,512,[],"f419","M316.3 452c-2.1 0-4.2-.6-6.1-1.6L291 439c-2.9-1.6-1.5-2.2-.5-2.5 3.8-1.3 4.6-1.6 8.7-4 .4-.2 1-.1 1.4.1l14.8 8.8c.5.3 1.3.3 1.8 0L375 408c.5-.3.9-.9.9-1.6v-66.7c0-.7-.3-1.3-.9-1.6l-57.8-33.3c-.5-.3-1.2-.3-1.8 0l-57.8 33.3c-.6.3-.9 1-.9 1.6v66.7c0 .6.4 1.2.9 1.5l15.8 9.1c8.6 4.3 13.9-.8 13.9-5.8v-65.9c0-.9.7-1.7 1.7-1.7h7.3c.9 0 1.7.7 1.7 1.7v65.9c0 11.5-6.2 18-17.1 18-3.3 0-6 0-13.3-3.6l-15.2-8.7c-3.7-2.2-6.1-6.2-6.1-10.5v-66.7c0-4.3 2.3-8.4 6.1-10.5l57.8-33.4c3.7-2.1 8.5-2.1 12.1 0l57.8 33.4c3.7 2.2 6.1 6.2 6.1 10.5v66.7c0 4.3-2.3 8.4-6.1 10.5l-57.8 33.4c-1.7 1.1-3.8 1.7-6 1.7zm46.7-65.8c0-12.5-8.4-15.8-26.2-18.2-18-2.4-19.8-3.6-19.8-7.8 0-3.5 1.5-8.1 14.8-8.1 11.9 0 16.3 2.6 18.1 10.6.2.8.8 1.3 1.6 1.3h7.5c.5 0 .9-.2 1.2-.5.3-.4.5-.8.4-1.3-1.2-13.8-10.3-20.2-28.8-20.2-16.5 0-26.3 7-26.3 18.6 0 12.7 9.8 16.1 25.6 17.7 18.9 1.9 20.4 4.6 20.4 8.3 0 6.5-5.2 9.2-17.4 9.2-15.3 0-18.7-3.8-19.8-11.4-.1-.8-.8-1.4-1.7-1.4h-7.5c-.9 0-1.7.7-1.7 1.7 0 9.7 5.3 21.3 30.6 21.3 18.5 0 29-7.2 29-19.8zm54.5-50.1c0 6.1-5 11.1-11.1 11.1s-11.1-5-11.1-11.1c0-6.3 5.2-11.1 11.1-11.1 6-.1 11.1 4.8 11.1 11.1zm-1.8 0c0-5.2-4.2-9.3-9.4-9.3-5.1 0-9.3 4.1-9.3 9.3 0 5.2 4.2 9.4 9.3 9.4 5.2-.1 9.4-4.3 9.4-9.4zm-4.5 6.2h-2.6c-.1-.6-.5-3.8-.5-3.9-.2-.7-.4-1.1-1.3-1.1h-2.2v5h-2.4v-12.5h4.3c1.5 0 4.4 0 4.4 3.3 0 2.3-1.5 2.8-2.4 3.1 1.7.1 1.8 1.2 2.1 2.8.1 1 .3 2.7.6 3.3zm-2.8-8.8c0-1.7-1.2-1.7-1.8-1.7h-2v3.5h1.9c1.6 0 1.9-1.1 1.9-1.8zM137.3 191c0-2.7-1.4-5.1-3.7-6.4l-61.3-35.3c-1-.6-2.2-.9-3.4-1h-.6c-1.2 0-2.3.4-3.4 1L3.7 184.6C1.4 185.9 0 188.4 0 191l.1 95c0 1.3.7 2.5 1.8 3.2 1.1.7 2.5.7 3.7 0L42 268.3c2.3-1.4 3.7-3.8 3.7-6.4v-44.4c0-2.6 1.4-5.1 3.7-6.4l15.5-8.9c1.2-.7 2.4-1 3.7-1 1.3 0 2.6.3 3.7 1l15.5 8.9c2.3 1.3 3.7 3.8 3.7 6.4v44.4c0 2.6 1.4 5.1 3.7 6.4l36.4 20.9c1.1.7 2.6.7 3.7 0 1.1-.6 1.8-1.9 1.8-3.2l.2-95zM472.5 87.3v176.4c0 2.6-1.4 5.1-3.7 6.4l-61.3 35.4c-2.3 1.3-5.1 1.3-7.4 0l-61.3-35.4c-2.3-1.3-3.7-3.8-3.7-6.4v-70.8c0-2.6 1.4-5.1 3.7-6.4l61.3-35.4c2.3-1.3 5.1-1.3 7.4 0l15.3 8.8c1.7 1 3.9-.3 3.9-2.2v-94c0-2.8 3-4.6 5.5-3.2l36.5 20.4c2.3 1.2 3.8 3.7 3.8 6.4zm-46 128.9c0-.7-.4-1.3-.9-1.6l-21-12.2c-.6-.3-1.3-.3-1.9 0l-21 12.2c-.6.3-.9.9-.9 1.6v24.3c0 .7.4 1.3.9 1.6l21 12.1c.6.3 1.3.3 1.8 0l21-12.1c.6-.3.9-.9.9-1.6v-24.3zm209.8-.7c2.3-1.3 3.7-3.8 3.7-6.4V192c0-2.6-1.4-5.1-3.7-6.4l-60.9-35.4c-2.3-1.3-5.1-1.3-7.4 0l-61.3 35.4c-2.3 1.3-3.7 3.8-3.7 6.4v70.8c0 2.7 1.4 5.1 3.7 6.4l60.9 34.7c2.2 1.3 5 1.3 7.3 0l36.8-20.5c2.5-1.4 2.5-5 0-6.4L550 241.6c-1.2-.7-1.9-1.9-1.9-3.2v-22.2c0-1.3.7-2.5 1.9-3.2l19.2-11.1c1.1-.7 2.6-.7 3.7 0l19.2 11.1c1.1.7 1.9 1.9 1.9 3.2v17.4c0 2.8 3.1 4.6 5.6 3.2l36.7-21.3zM559 219c-.4.3-.7.7-.7 1.2v13.6c0 .5.3 1 .7 1.2l11.8 6.8c.4.3 1 .3 1.4 0L584 235c.4-.3.7-.7.7-1.2v-13.6c0-.5-.3-1-.7-1.2l-11.8-6.8c-.4-.3-1-.3-1.4 0L559 219zm-254.2 43.5v-70.4c0-2.6-1.6-5.1-3.9-6.4l-61.1-35.2c-2.1-1.2-5-1.4-7.4 0l-61.1 35.2c-2.3 1.3-3.9 3.7-3.9 6.4v70.4c0 2.8 1.9 5.2 4 6.4l61.2 35.2c2.4 1.4 5.2 1.3 7.4 0l61-35.2c1.8-1 3.1-2.7 3.6-4.7.1-.5.2-1.1.2-1.7zm-74.3-124.9l-.8.5h1.1l-.3-.5zm76.2 130.2l-.4-.7v.9l.4-.2z"]},E1={prefix:"fab",iconName:"yarn",icon:[496,512,[],"f7e3","M393.9 345.2c-39 9.3-48.4 32.1-104 47.4 0 0-2.7 4-10.4 5.8-13.4 3.3-63.9 6-68.5 6.1-12.4.1-19.9-3.2-22-8.2-6.4-15.3 9.2-22 9.2-22-8.1-5-9-9.9-9.8-8.1-2.4 5.8-3.6 20.1-10.1 26.5-8.8 8.9-25.5 5.9-35.3.8-10.8-5.7.8-19.2.8-19.2s-5.8 3.4-10.5-3.6c-6-9.3-17.1-37.3 11.5-62-1.3-10.1-4.6-53.7 40.6-85.6 0 0-20.6-22.8-12.9-43.3 5-13.4 7-13.3 8.6-13.9 5.7-2.2 11.3-4.6 15.4-9.1 20.6-22.2 46.8-18 46.8-18s12.4-37.8 23.9-30.4c3.5 2.3 16.3 30.6 16.3 30.6s13.6-7.9 15.1-5c8.2 16 9.2 46.5 5.6 65.1-6.1 30.6-21.4 47.1-27.6 57.5-1.4 2.4 16.5 10 27.8 41.3 10.4 28.6 1.1 52.7 2.8 55.3.8 1.4 13.7.8 36.4-13.2 12.8-7.9 28.1-16.9 45.4-17 16.7-.5 17.6 19.2 4.9 22.2zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-79.3 75.2c-1.7-13.6-13.2-23-28-22.8-22 .3-40.5 11.7-52.8 19.2-4.8 3-8.9 5.2-12.4 6.8 3.1-44.5-22.5-73.1-28.7-79.4 7.8-11.3 18.4-27.8 23.4-53.2 4.3-21.7 3-55.5-6.9-74.5-1.6-3.1-7.4-11.2-21-7.4-9.7-20-13-22.1-15.6-23.8-1.1-.7-23.6-16.4-41.4 28-12.2.9-31.3 5.3-47.5 22.8-2 2.2-5.9 3.8-10.1 5.4h.1c-8.4 3-12.3 9.9-16.9 22.3-6.5 17.4.2 34.6 6.8 45.7-17.8 15.9-37 39.8-35.7 82.5-34 36-11.8 73-5.6 79.6-1.6 11.1 3.7 19.4 12 23.8 12.6 6.7 30.3 9.6 43.9 2.8 4.9 5.2 13.8 10.1 30 10.1 6.8 0 58-2.9 72.6-6.5 6.8-1.6 11.5-4.5 14.6-7.1 9.8-3.1 36.8-12.3 62.2-28.7 18-11.7 24.2-14.2 37.6-17.4 12.9-3.2 21-15.1 19.4-28.2z"]},C1={prefix:"fab",iconName:"react",icon:[512,512,[],"f41b","M418.2 177.2c-5.4-1.8-10.8-3.5-16.2-5.1.9-3.7 1.7-7.4 2.5-11.1 12.3-59.6 4.2-107.5-23.1-123.3-26.3-15.1-69.2.6-112.6 38.4-4.3 3.7-8.5 7.6-12.5 11.5-2.7-2.6-5.5-5.2-8.3-7.7-45.5-40.4-91.1-57.4-118.4-41.5-26.2 15.2-34 60.3-23 116.7 1.1 5.6 2.3 11.1 3.7 16.7-6.4 1.8-12.7 3.8-18.6 5.9C38.3 196.2 0 225.4 0 255.6c0 31.2 40.8 62.5 96.3 81.5 4.5 1.5 9 3 13.6 4.3-1.5 6-2.8 11.9-4 18-10.5 55.5-2.3 99.5 23.9 114.6 27 15.6 72.4-.4 116.6-39.1 3.5-3.1 7-6.3 10.5-9.7 4.4 4.3 9 8.4 13.6 12.4 42.8 36.8 85.1 51.7 111.2 36.6 27-15.6 35.8-62.9 24.4-120.5-.9-4.4-1.9-8.9-3-13.5 3.2-.9 6.3-1.9 9.4-2.9 57.7-19.1 99.5-50 99.5-81.7 0-30.3-39.4-59.7-93.8-78.4zM282.9 92.3c37.2-32.4 71.9-45.1 87.7-36 16.9 9.7 23.4 48.9 12.8 100.4-.7 3.4-1.4 6.7-2.3 10-22.2-5-44.7-8.6-67.3-10.6-13-18.6-27.2-36.4-42.6-53.1 3.9-3.7 7.7-7.2 11.7-10.7zM167.2 307.5c5.1 8.7 10.3 17.4 15.8 25.9-15.6-1.7-31.1-4.2-46.4-7.5 4.4-14.4 9.9-29.3 16.3-44.5 4.6 8.8 9.3 17.5 14.3 26.1zm-30.3-120.3c14.4-3.2 29.7-5.8 45.6-7.8-5.3 8.3-10.5 16.8-15.4 25.4-4.9 8.5-9.7 17.2-14.2 26-6.3-14.9-11.6-29.5-16-43.6zm27.4 68.9c6.6-13.8 13.8-27.3 21.4-40.6s15.8-26.2 24.4-38.9c15-1.1 30.3-1.7 45.9-1.7s31 .6 45.9 1.7c8.5 12.6 16.6 25.5 24.3 38.7s14.9 26.7 21.7 40.4c-6.7 13.8-13.9 27.4-21.6 40.8-7.6 13.3-15.7 26.2-24.2 39-14.9 1.1-30.4 1.6-46.1 1.6s-30.9-.5-45.6-1.4c-8.7-12.7-16.9-25.7-24.6-39s-14.8-26.8-21.5-40.6zm180.6 51.2c5.1-8.8 9.9-17.7 14.6-26.7 6.4 14.5 12 29.2 16.9 44.3-15.5 3.5-31.2 6.2-47 8 5.4-8.4 10.5-17 15.5-25.6zm14.4-76.5c-4.7-8.8-9.5-17.6-14.5-26.2-4.9-8.5-10-16.9-15.3-25.2 16.1 2 31.5 4.7 45.9 8-4.6 14.8-10 29.2-16.1 43.4zM256.2 118.3c10.5 11.4 20.4 23.4 29.6 35.8-19.8-.9-39.7-.9-59.5 0 9.8-12.9 19.9-24.9 29.9-35.8zM140.2 57c16.8-9.8 54.1 4.2 93.4 39 2.5 2.2 5 4.6 7.6 7-15.5 16.7-29.8 34.5-42.9 53.1-22.6 2-45 5.5-67.2 10.4-1.3-5.1-2.4-10.3-3.5-15.5-9.4-48.4-3.2-84.9 12.6-94zm-24.5 263.6c-4.2-1.2-8.3-2.5-12.4-3.9-21.3-6.7-45.5-17.3-63-31.2-10.1-7-16.9-17.8-18.8-29.9 0-18.3 31.6-41.7 77.2-57.6 5.7-2 11.5-3.8 17.3-5.5 6.8 21.7 15 43 24.5 63.6-9.6 20.9-17.9 42.5-24.8 64.5zm116.6 98c-16.5 15.1-35.6 27.1-56.4 35.3-11.1 5.3-23.9 5.8-35.3 1.3-15.9-9.2-22.5-44.5-13.5-92 1.1-5.6 2.3-11.2 3.7-16.7 22.4 4.8 45 8.1 67.9 9.8 13.2 18.7 27.7 36.6 43.2 53.4-3.2 3.1-6.4 6.1-9.6 8.9zm24.5-24.3c-10.2-11-20.4-23.2-30.3-36.3 9.6.4 19.5.6 29.5.6 10.3 0 20.4-.2 30.4-.7-9.2 12.7-19.1 24.8-29.6 36.4zm130.7 30c-.9 12.2-6.9 23.6-16.5 31.3-15.9 9.2-49.8-2.8-86.4-34.2-4.2-3.6-8.4-7.5-12.7-11.5 15.3-16.9 29.4-34.8 42.2-53.6 22.9-1.9 45.7-5.4 68.2-10.5 1 4.1 1.9 8.2 2.7 12.2 4.9 21.6 5.7 44.1 2.5 66.3zm18.2-107.5c-2.8.9-5.6 1.8-8.5 2.6-7-21.8-15.6-43.1-25.5-63.8 9.6-20.4 17.7-41.4 24.5-62.9 5.2 1.5 10.2 3.1 15 4.7 46.6 16 79.3 39.8 79.3 58 0 19.6-34.9 44.9-84.8 61.4zm-149.7-15c25.3 0 45.8-20.5 45.8-45.8s-20.5-45.8-45.8-45.8c-25.3 0-45.8 20.5-45.8 45.8s20.5 45.8 45.8 45.8z"]},P1={prefix:"fab",iconName:"linkedin",icon:[448,512,[],"f08c","M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"]},A1={prefix:"fab",iconName:"jira",icon:[496,512,[],"f7b1","M490 241.7C417.1 169 320.6 71.8 248.5 0 83 164.9 6 241.7 6 241.7c-7.9 7.9-7.9 20.7 0 28.7C138.8 402.7 67.8 331.9 248.5 512c379.4-378 15.7-16.7 241.5-241.7 8-7.9 8-20.7 0-28.6zm-241.5 90l-76-75.7 76-75.7 76 75.7-76 75.7z"]},N1={prefix:"fab",iconName:"html5",icon:[384,512,[],"f13b","M0 32l34.9 395.8L191.5 480l157.6-52.2L384 32H0zm308.2 127.9H124.4l4.1 49.4h175.6l-13.6 148.4-97.9 27v.3h-1.1l-98.7-27.3-6-75.8h47.7L138 320l53.5 14.5 53.7-14.5 6-62.2H84.3L71.5 112.2h241.1l-4.4 47.7z"]},_1={prefix:"fab",iconName:"trello",icon:[448,512,[],"f181","M392.3 32H56.1C25.1 32 0 57.1 0 88c-.1 0 0-4 0 336 0 30.9 25.1 56 56 56h336.2c30.8-.2 55.7-25.2 55.7-56V88c.1-30.8-24.8-55.8-55.6-56zM197 371.3c-.2 14.7-12.1 26.6-26.9 26.6H87.4c-14.8.1-26.9-11.8-27-26.6V117.1c0-14.8 12-26.9 26.9-26.9h82.9c14.8 0 26.9 12 26.9 26.9v254.2zm193.1-112c0 14.8-12 26.9-26.9 26.9h-81c-14.8 0-26.9-12-26.9-26.9V117.2c0-14.8 12-26.9 26.8-26.9h81.1c14.8 0 26.9 12 26.9 26.9v142.1z"]},j1={prefix:"fab",iconName:"js",icon:[448,512,[],"f3b8","M0 32v448h448V32H0zm243.8 349.4c0 43.6-25.6 63.5-62.9 63.5-33.7 0-53.2-17.4-63.2-38.5l34.3-20.7c6.6 11.7 12.6 21.6 27.1 21.6 13.8 0 22.6-5.4 22.6-26.5V237.7h42.1v143.7zm99.6 63.5c-39.1 0-64.4-18.6-76.7-43l34.3-19.8c9 14.7 20.8 25.6 41.5 25.6 17.4 0 28.6-8.7 28.6-20.8 0-14.4-11.4-19.5-30.7-28l-10.5-4.5c-30.4-12.9-50.5-29.2-50.5-63.5 0-31.6 24.1-55.6 61.6-55.6 26.8 0 46 9.3 59.8 33.7L368 290c-7.2-12.9-15-18-27.1-18-12.3 0-20.1 7.8-20.1 18 0 12.6 7.8 17.7 25.9 25.6l10.5 4.5c35.8 15.3 55.9 31 55.9 66.2 0 37.8-29.8 58.6-69.7 58.6z"]},z1={prefix:"fab",iconName:"git",icon:[512,512,[],"f1d3","M216.29 158.39H137C97 147.9 6.51 150.63 6.51 233.18c0 30.09 15 51.23 35 61-25.1 23-37 33.85-37 49.21 0 11 4.47 21.14 17.89 26.81C8.13 383.61 0 393.35 0 411.65c0 32.11 28.05 50.82 101.63 50.82 70.75 0 111.79-26.42 111.79-73.18 0-58.66-45.16-56.5-151.63-63l13.43-21.55c27.27 7.58 118.7 10 118.7-67.89 0-18.7-7.73-31.71-15-41.07l37.41-2.84zm-63.42 241.9c0 32.06-104.89 32.1-104.89 2.43 0-8.14 5.27-15 10.57-21.54 77.71 5.3 94.32 3.37 94.32 19.11zm-50.81-134.58c-52.8 0-50.46-71.16 1.2-71.16 49.54 0 50.82 71.16-1.2 71.16zm133.3 100.51v-32.1c26.75-3.66 27.24-2 27.24-11V203.61c0-8.5-2.05-7.38-27.24-16.26l4.47-32.92H324v168.71c0 6.51.4 7.32 6.51 8.14l20.73 2.84v32.1zm52.45-244.31c-23.17 0-36.59-13.43-36.59-36.61s13.42-35.77 36.59-35.77c23.58 0 37 12.62 37 35.77s-13.42 36.61-37 36.61zM512 350.46c-17.49 8.53-43.1 16.26-66.28 16.26-48.38 0-66.67-19.5-66.67-65.46V194.75c0-5.42 1.05-4.06-31.71-4.06V154.5c35.78-4.07 50-22 54.47-66.27h38.63c0 65.83-1.34 61.81 3.26 61.81H501v40.65h-60.56v97.15c0 6.92-4.92 51.41 60.57 26.84z"]},R1={prefix:"fab",iconName:"sketch",icon:[512,512,[],"f7c6","M27.5 162.2L9 187.1h90.5l6.9-130.7-78.9 105.8zM396.3 45.7L267.7 32l135.7 147.2-7.1-133.5zM112.2 218.3l-11.2-22H9.9L234.8 458zm2-31.2h284l-81.5-88.5L256.3 33zm297.3 9.1L277.6 458l224.8-261.7h-90.9zM415.4 69L406 56.4l.9 17.3 6.1 113.4h90.3zM113.5 93.5l-4.6 85.6L244.7 32 116.1 45.7zm287.7 102.7h-290l42.4 82.9L256.3 480l144.9-283.8z"]},Bh={prefix:"fab",iconName:"github",icon:[496,512,[],"f09b","M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"]},O1={prefix:"fab",iconName:"npm",icon:[576,512,[],"f3d4","M288 288h-32v-64h32v64zm288-128v192H288v32H160v-32H0V160h576zm-416 32H32v128h64v-96h32v96h32V192zm160 0H192v160h64v-32h64V192zm224 0H352v128h64v-96h32v96h32v-96h32v96h32V192z"]},M1={prefix:"fab",iconName:"medium",icon:[640,512,[62407,"medium-m"],"f23a","M180.5,74.262C80.813,74.262,0,155.633,0,256S80.819,437.738,180.5,437.738,361,356.373,361,256,280.191,74.262,180.5,74.262Zm288.25,10.646c-49.845,0-90.245,76.619-90.245,171.095s40.406,171.1,90.251,171.1,90.251-76.619,90.251-171.1H559C559,161.5,518.6,84.908,468.752,84.908Zm139.506,17.821c-17.526,0-31.735,68.628-31.735,153.274s14.2,153.274,31.735,153.274S640,340.631,640,256C640,171.351,625.785,102.729,608.258,102.729Z"]},F1={prefix:"fas",iconName:"mug-hot",icon:[512,512,[9749],"f7b6","M88 0C74.7 0 64 10.7 64 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C120.5 112.3 128 119.9 128 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C119.5 47.7 112 40.1 112 24c0-13.3-10.7-24-24-24zM32 192c-17.7 0-32 14.3-32 32V416c0 53 43 96 96 96H288c53 0 96-43 96-96h16c61.9 0 112-50.1 112-112s-50.1-112-112-112H352 32zm352 64h16c26.5 0 48 21.5 48 48s-21.5 48-48 48H384V256zM224 24c0-13.3-10.7-24-24-24s-24 10.7-24 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C232.5 112.3 240 119.9 240 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C231.5 47.7 224 40.1 224 24z"]},L1={prefix:"fas",iconName:"envelope",icon:[512,512,[128386,9993,61443],"f0e0","M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"]},D1={prefix:"fas",iconName:"keyboard",icon:[576,512,[9e3],"f11c","M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zm16 64h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM64 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V240zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V336c0-8.8 7.2-16 16-16zm80-176c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V144zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zM160 336c0-8.8 7.2-16 16-16H400c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V336zM272 128h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H272c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM256 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H272c-8.8 0-16-7.2-16-16V240zM368 128h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM352 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V240zM464 128h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM448 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V240zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V336c0-8.8 7.2-16 16-16z"]},H1={prefix:"fas",iconName:"file",icon:[384,512,[128196,128459,61462],"f15b","M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0L384 128z"]};const W1=[{name:"React",logo:C1,link:"https://react.dev/"},{name:"Yarn",logo:E1,link:"https://classic.yarnpkg.com/lang/en/"},{name:"Javascript",logo:j1,link:"https://www.javascript.com/"},{name:"CSS",logo:I1,link:"https://developer.mozilla.org/en-US/docs/Web/CSS"},{name:"Git",logo:z1,link:"https://git-scm.com/"},{name:"JIRA",logo:A1,link:"https://www.atlassian.com/software/jira"},{name:"Node",logo:T1,link:"https://nodejs.org/en"},{name:"Github",logo:Bh,link:"https://github.com/"},{name:"HTML5",logo:N1,link:"https://developer.mozilla.org/en-US/docs/Glossary/HTML5"},{name:"Trello",logo:_1,link:"https://www.atlassian.com/software/trello"},{name:"TypeScript",logo:D1,link:"https://www.typescriptlang.org/"},{name:"Sketch",logo:R1,link:"https://www.sketch.com/"},{name:"Mocha",logo:F1,link:"https://mochajs.org/"},{name:"npm",logo:O1,link:"https://npmjs.com/"},{name:"Markdown",logo:x1,link:"https://www.markdownguide.org/"}];function V1(){return de("div",{children:[A("h2",{id:"tech-stack-h2",children:"Tech Stack"}),A("div",{id:"tech-stack",children:W1.map(e=>A("div",{className:"techIconDiv",children:de("a",{href:e.link,target:"_blank",children:[A(Ln,{className:"techIcon",icon:e.logo}),A("p",{className:"hide",children:e.name})]})},e.name))})]})}const U1="Stories by Spencer Attick on Medium",G1="Stories by Spencer Attick on Medium",B1="https://medium.com/@spencer.attick?source=rss-e5dc359f27c2------2",Y1="https://cdn-images-1.medium.com/fit/c/150/150/1*-uzWbooKuJ4W-gL2TBuWkA.png",J1=[],$1=[{id:"https://medium.com/p/17f6e29a07ba",title:"A Guide to Immediately Invoked Function Expressions (IIFE)",link:"https://medium.com/@spencer.attick/a-guide-to-immediately-invoked-function-expressions-iife-17f6e29a07ba?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1681325789e3,created:1681325789e3,category:["privacy","javascript","security","code","software-development"],content:'

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log("Hello, I am a self-invoking arrow function!");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = \'secret\';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log("Returning cached data");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log("Fetching data from server");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

',enclosures:[],content_encoded:'

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log("Hello, I am a self-invoking arrow function!");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = \'secret\';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log("Returning cached data");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log("Fetching data from server");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

',media:{}},{id:"https://medium.com/p/c3214c897b4c",title:"Project: Script to Automatically Update Resources in my Portfolio",link:"https://medium.com/@spencer.attick/project-script-to-automatically-update-resources-in-my-portfolio-c3214c897b4c?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1681220946e3,created:1681220946e3,category:["portfolio","nodejs","software-development","javascript","script"],content:'

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

',enclosures:[],content_encoded:'

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

',media:{}},{id:"https://medium.com/p/a4c2e35b2558",title:"Project: Creating Code Examples for Segment’s Server-Side Libraries",link:"https://medium.com/@spencer.attick/project-creating-code-examples-for-all-of-segments-server-side-libraries-a4c2e35b2558?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1680791081e3,created:1680791081e3,category:["python","segment","servers","ruby","nodejs"],content:'

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

',enclosures:[],content_encoded:'

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

',media:{}},{id:"https://medium.com/p/fc36c10c02e5",title:"Project: Story Points",link:"https://medium.com/@spencer.attick/project-story-points-fc36c10c02e5?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1680618193e3,created:1680618193e3,category:["javascript","support","optimization","zendesk","story-points"],content:'

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

From there, logic is in place to add points up based on which fields are used:

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

',enclosures:[],content_encoded:'

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

From there, logic is in place to add points up based on which fields are used:

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

',media:{}},{id:"https://medium.com/p/f5efe0afd046",title:"Project: Updating Segment’s analytics-node Library to Include TypeScript",link:"https://medium.com/@spencer.attick/project-updating-segments-analytics-node-library-to-include-typescript-f5efe0afd046?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1678993785e3,created:1678993785e3,category:["segment","codecademy","node","typescript"],content:'

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

',enclosures:[],content_encoded:'

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

',media:{}},{id:"https://medium.com/p/9251d1438121",title:"Project: Segment Source Function — Auth0",link:"https://medium.com/@spencer.attick/project-segment-source-function-auth0-9251d1438121?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1678737033e3,created:1678737033e3,category:["auth0","javascript","node","segment"],content:'

Project: Segment Source Function — Auth0

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

',enclosures:[],content_encoded:'

Project: Segment Source Function — Auth0

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

',media:{}},{id:"https://medium.com/p/7f03b1b9e1b9",title:"Scope, Hoisting, and Closure in Javascript",link:"https://medium.com/@spencer.attick/scope-and-hoisting-and-closure-in-javascript-7f03b1b9e1b9?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1675365832e3,created:1675365832e3,category:["javascript-tips","hoisting","javascript","scopes","closure"],content:`

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

`,enclosures:[],content_encoded:`

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

`,media:{}},{id:"https://medium.com/p/a8add44f764d",title:"Memory & Automatic Memory Management (Garbage Collection) in Javascript",link:"https://medium.com/@spencer.attick/memory-automatic-memory-management-garbage-collection-in-javascript-a8add44f764d?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1674931467e3,created:1674931467e3,category:["javascript","chrome","v8-engine","scavenging","mark-and-sweep"],content:`

What is memory allocation? As a programmer working for years with languages like Javascript and Python, memory usage and management hasn’t been something I’ve had to think much about and, quite frankly, have taken for granted. Not knowing what it is or how it works has left me out of the loop in terms of the inner workings of how a few very important principles work in Javascript specifically.

So let’s take a look!

What is memory management?

All software programs use memory in some form or another. When we’re talking about memory we’re referring to physical space that is needed to define and run files on a computer. These days, technology is improving more and more to the point where the physical components that store and process memory are smaller than every before which means that even something as the size of your phone is extremely powerful in terms of what it can do with the physical memory allocation it has (especially compared to the room-sized computers of the past— check out a visual history of computers here). To control what is using space on a device, memory management is used.

Memory management refers to the process by which variable values and other references are removed from memory when a program no longer uses them. For example:

let testVar = 'Some string';
testVar = null;

On the first line, we’ve declared a variable testVar and have given it the value 'Some string’ . That value, 'Some string', needs to live somewhere in a computer’s memory so it can be referenced later. However, on the second line, we’re setting testVar to null which removes the reference of testVar to 'Some string' making 'Some string' a piece of data that now lives in memory without a useable reference to it. Remember, memory usage on a computer denotes a usage of physical space so we can’t just leave 'Some string' floating around in memory indefinitely. If we did, all of those unreferenced pieces of data would eventually take up all of the available space a computer has for memory and then your computer would become useless (rather needlessly).

Some programming languages require the engineer to manage the memory needed to store data in things like variables manually. These languages are closer to machine code in nature and are known as low-level languages, such as C++. Others, know as high-level languages like Javascript, Python, and Ruby, have much of the complexity of managing memory on a program abstracted away by making those processes automatic.

How is memory stored in Javascript?

Javascript has two mechanisms for storing memory: the stack and the heap.

The Stack

Primitive data types (string, number, boolean, etc.) have their values stored in the stack along with their keys or references.

For example the following would be stored in a stack:

A stack allocates a constant amount of data per item which means primitive types have a limit in terms of how large they can become which is known as static memory allocation.

The Heap

We’ve seen that the stack will hold references to data and the data itself for primitive data types. What about objects, functions, and arrays?

We call these reference data types in terms of memory management. This is because the stack will hold references to objects, functions, and arrays but will not store those values itself. Reference data types have their values stored in a heap.

Here we can see that the reference types listed in the stack groceryList (an array) and menu (an object), have a reference in the stack but their values in the heap:

The heap can do what’s called dynamic memory allocation to adjust for the space that a reference data type is using. The only limit to the amount of space a reference data type can use is the amount of total memory available for usage.

Why does this matter for day-to-day Javascript programming?

As a Javascript developer, you need to keep this storage system in mind as it impacts how your variables are stored and updated.

For example, take a look at this code:

Notice that setting newMenu to menu and then updating newMenu actually updates both newMenu and menu. That is because when you assign newMenu to menu, they’re then both pointing to the same exact object in the heap.

Be careful with this though. Consider the following example:

We have the same situation here as with the first example. The newMenu variable points at menu. So why, when the console.logs() are run, do both variables not resolve to an empty object since newMenu was set to {} on line 9? Aren’t both menu and newMenu pointing to the same spot in memory?

They were! But on line 9 when newMenu was set equal to {}, it’s actually being reassigned to a new object. Now menu is still pointing at the original object but, by using the = assignment operator, newMenu is now pointing at a new empty object so the variables are no longer connected.

This works the same way with arrays and functions:

Now that we know a little more about what memory in Javascript looks like, what happens when we have a reference that’s no longer used?

Garbage Collection

Automatic Memory Management (known more colloquially as garbage collection) is the system by which Javascript engines decide which references no longer exist and remove them. This happens out-of-the-box and in such a way that engineers usually don’t need to be too concerned with it their day to day work.

Javascript’s V8 engine (which powers Chrome and Node.js) uses a method called generational garbage collection which involves processes called scavenging and mark-and-sweep.

Generational Garbage Collection

The V8 engine keeps track of new and old data (referred to as “generations”) by taking brand new data and running it through a process called scavenging. It will run its scavenging mechanism several times on the same data (new generation data) such that data that continues to be referenced will be kept and data that loses its references will be removed from memory. Data that remains after several scavenging cycles will be moved out of the space that new data had been occupying and will be stored in a longer term manner where garbage collection, called mark-and-sweep at this next phase, will happen more infrequently. This data is now considered old generation data.

Scavenging

Brand new data coming into memory is scavenged several times before memory graduates to become old generation in memory. During the process of scavenging, data with existing references are copied to a new space. The previous space where this data existed is cleared of data that no longer has references and that space is then ready to ingest data that has yet to be seen (even newer data) into memory. This scavenging process will happen to data a few times before data is moved considered “old generation”. Scavenging is a fast method of garbage collection that happens with new data that is most likely to lose its reference quickly. Once data graduates to old generation it is then no longer scavenged but is subject to the more infrequent process of mark-and-sweep instead.

Mark-and-Sweep

With mark-and-sweep, the Javascript garbage collector mechanism will traverse all data in the old generation of memory and will “mark” any data that has an existing reference with which that data is reachable by programs being run on a machine. Then, in its second phase of operation, any data that has not been marked will be cleared away and removed from memory.

Resources

https://www.youtube.com/watch?v=Hci9Bb4_fkA (great video about memory)

https://www.youtube.com/watch?v=AeUCN2lPqL8 (pretty good — longer and a little less straightforward)

https://www.youtube.com/watch?v=DIzouoy13UM

https://felixgerschau.com/javascript-memory-management/ (very good)

https://www.youtube.com/watch?v=FZkCh_GeftY (good examples/explanation of reference-counting and mark and sweep)

https://www.geeksforgeeks.org/memory-management-in-javascript/

`,enclosures:[],content_encoded:`

What is memory allocation? As a programmer working for years with languages like Javascript and Python, memory usage and management hasn’t been something I’ve had to think much about and, quite frankly, have taken for granted. Not knowing what it is or how it works has left me out of the loop in terms of the inner workings of how a few very important principles work in Javascript specifically.

So let’s take a look!

What is memory management?

All software programs use memory in some form or another. When we’re talking about memory we’re referring to physical space that is needed to define and run files on a computer. These days, technology is improving more and more to the point where the physical components that store and process memory are smaller than every before which means that even something as the size of your phone is extremely powerful in terms of what it can do with the physical memory allocation it has (especially compared to the room-sized computers of the past— check out a visual history of computers here). To control what is using space on a device, memory management is used.

Memory management refers to the process by which variable values and other references are removed from memory when a program no longer uses them. For example:

let testVar = 'Some string';
testVar = null;

On the first line, we’ve declared a variable testVar and have given it the value 'Some string’ . That value, 'Some string', needs to live somewhere in a computer’s memory so it can be referenced later. However, on the second line, we’re setting testVar to null which removes the reference of testVar to 'Some string' making 'Some string' a piece of data that now lives in memory without a useable reference to it. Remember, memory usage on a computer denotes a usage of physical space so we can’t just leave 'Some string' floating around in memory indefinitely. If we did, all of those unreferenced pieces of data would eventually take up all of the available space a computer has for memory and then your computer would become useless (rather needlessly).

Some programming languages require the engineer to manage the memory needed to store data in things like variables manually. These languages are closer to machine code in nature and are known as low-level languages, such as C++. Others, know as high-level languages like Javascript, Python, and Ruby, have much of the complexity of managing memory on a program abstracted away by making those processes automatic.

How is memory stored in Javascript?

Javascript has two mechanisms for storing memory: the stack and the heap.

The Stack

Primitive data types (string, number, boolean, etc.) have their values stored in the stack along with their keys or references.

For example the following would be stored in a stack:

A stack allocates a constant amount of data per item which means primitive types have a limit in terms of how large they can become which is known as static memory allocation.

The Heap

We’ve seen that the stack will hold references to data and the data itself for primitive data types. What about objects, functions, and arrays?

We call these reference data types in terms of memory management. This is because the stack will hold references to objects, functions, and arrays but will not store those values itself. Reference data types have their values stored in a heap.

Here we can see that the reference types listed in the stack groceryList (an array) and menu (an object), have a reference in the stack but their values in the heap:

The heap can do what’s called dynamic memory allocation to adjust for the space that a reference data type is using. The only limit to the amount of space a reference data type can use is the amount of total memory available for usage.

Why does this matter for day-to-day Javascript programming?

As a Javascript developer, you need to keep this storage system in mind as it impacts how your variables are stored and updated.

For example, take a look at this code:

Notice that setting newMenu to menu and then updating newMenu actually updates both newMenu and menu. That is because when you assign newMenu to menu, they’re then both pointing to the same exact object in the heap.

Be careful with this though. Consider the following example:

We have the same situation here as with the first example. The newMenu variable points at menu. So why, when the console.logs() are run, do both variables not resolve to an empty object since newMenu was set to {} on line 9? Aren’t both menu and newMenu pointing to the same spot in memory?

They were! But on line 9 when newMenu was set equal to {}, it’s actually being reassigned to a new object. Now menu is still pointing at the original object but, by using the = assignment operator, newMenu is now pointing at a new empty object so the variables are no longer connected.

This works the same way with arrays and functions:

Now that we know a little more about what memory in Javascript looks like, what happens when we have a reference that’s no longer used?

Garbage Collection

Automatic Memory Management (known more colloquially as garbage collection) is the system by which Javascript engines decide which references no longer exist and remove them. This happens out-of-the-box and in such a way that engineers usually don’t need to be too concerned with it their day to day work.

Javascript’s V8 engine (which powers Chrome and Node.js) uses a method called generational garbage collection which involves processes called scavenging and mark-and-sweep.

Generational Garbage Collection

The V8 engine keeps track of new and old data (referred to as “generations”) by taking brand new data and running it through a process called scavenging. It will run its scavenging mechanism several times on the same data (new generation data) such that data that continues to be referenced will be kept and data that loses its references will be removed from memory. Data that remains after several scavenging cycles will be moved out of the space that new data had been occupying and will be stored in a longer term manner where garbage collection, called mark-and-sweep at this next phase, will happen more infrequently. This data is now considered old generation data.

Scavenging

Brand new data coming into memory is scavenged several times before memory graduates to become old generation in memory. During the process of scavenging, data with existing references are copied to a new space. The previous space where this data existed is cleared of data that no longer has references and that space is then ready to ingest data that has yet to be seen (even newer data) into memory. This scavenging process will happen to data a few times before data is moved considered “old generation”. Scavenging is a fast method of garbage collection that happens with new data that is most likely to lose its reference quickly. Once data graduates to old generation it is then no longer scavenged but is subject to the more infrequent process of mark-and-sweep instead.

Mark-and-Sweep

With mark-and-sweep, the Javascript garbage collector mechanism will traverse all data in the old generation of memory and will “mark” any data that has an existing reference with which that data is reachable by programs being run on a machine. Then, in its second phase of operation, any data that has not been marked will be cleared away and removed from memory.

Resources

https://www.youtube.com/watch?v=Hci9Bb4_fkA (great video about memory)

https://www.youtube.com/watch?v=AeUCN2lPqL8 (pretty good — longer and a little less straightforward)

https://www.youtube.com/watch?v=DIzouoy13UM

https://felixgerschau.com/javascript-memory-management/ (very good)

https://www.youtube.com/watch?v=FZkCh_GeftY (good examples/explanation of reference-counting and mark and sweep)

https://www.geeksforgeeks.org/memory-management-in-javascript/

`,media:{}},{id:"https://medium.com/p/5f5bfabd3a89",title:"A First Look at the Javascript Event Loop on the Browser",link:"https://medium.com/@spencer.attick/a-first-look-at-the-javascript-event-loop-on-the-browser-5f5bfabd3a89?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1674060961e3,created:1674060961e3,category:["programming","javascript","javascript-event-loop","code","event-loop"],content:'

When you execute Javascript on the client, there are some behind-the-scenes processes that make the magic happen. As Javascript is a single-threaded language (it can only do one thing at a time), it needs a way to be told what aspects of your codebase should be run immediately, which should wait on things like API responses and timeouts, and in what order all of that should happen.

Enter, the Event Loop!

The Event Loop takes care of managing the execution of your program in a way that accounts for asynchronous behavior, the nesting of functions, and more.

What does the Event Loop look like?

At a high level, we can think of the Event Loop like this:

Before we step through any code, let’s align on each of these pieces.

The Code Input will simply be the Javascript file you intend to run.

The Call Stack refers to a mechanism that processes each element in your code. It is a stack data structure which is known as first-in-last-out. That means that the first item that is pushed into the Call Stack (as with any stack) will be processed last. It’s like if you went to the library and searched the selves for books you wanted to take home. If you placed each book you found on top of the last, you’d have a pile of books with the first book you took off of the selves on the bottom and the most recent book you found on top. If you handed that pile to the librarian, they would likely take the top book from the pile to check it out for you first. Operating in that manner, they would check out the FIRST book you found on the shelves LAST (after checking all the other books out for you).

The Outside API/Javascript API section refers to an act of using one of the API methods available in the browser (like setTimeout) or the use of another outside API. When requests are made out to APIs, those actions are generally considered asynchronous as they deviate from Javascript running and completing tasks line by line, in order (synchronously).

The Event Queue is a queue data structure similar to a stack, but rather than being first-in-last-out, a queue is first-in-first-out. In terms of a queue, you can think of waiting in a line at the grocery store. If you enter the line first, the cashier will help you to check out before everyone else in the line. If you enter the line last, you’ll have to wait for each customer in front of you to check out, in order, before you’ll be allowed to check out. The first person in the line is the first person to have their groceries scanned and to leave. All programming queues work in this way, including the Event Queue.

The Event Loop is a mechanism that keeps track of whether or not there are any operations needing to be processed in the Event Queue. If there are items that need handling in the queue, it will loop through them and add them to the Call Stack one at a time. The Event Loop can only add items to the Call Stack if the Call Stack is totally empty.

Finally, the Code Output will help us visualize how the Code Input is run in terms of letting us see the order of operations. Consider this depiction to be something like running a file from your browser to see what prints in the console out and when.

How does the Event Loop work?

Now that we have a sense of the different parts of this process and a high-level understanding of each, let’s look at an example to help us understand how a file is read in Javascript and how each line is fed through the Event Loop.

To begin, the first line is a console.log() which is pushed into the Call Stack:

There isn’t any process to wait on here in order for the console.log() to complete, so it’s immediately resolved from the stack and the result is displayed in the Code Output section of this diagram but you can imagine this would be printed to the console of the browser:

With that, the first line is processed and First line to run! is printed out.

Next we’ll look at a setTimeout() function which actually makes use of a timer API available in the browser to keep track of when the milliseconds passed in as the second argument to that function have transpired:

First, the setTimeout() is added to the Call Stack. If you’re unfamiliar, this function takes two arguments: a function to execute and a number to represent the amount of milliseconds to wait until the function passed in as the first argument is called.

The mechanism that keeps track of the time here is a built-in browser API. As that is the case, the next step will be to invoke that timer on the browser itself:

With the timer set to run on the browser, the setTimeout() function is finished being processed in the Call Stack. Next we can look at the console.log() on line 7 while wait for the browser API to return the callback we passed to setTimeout() . I encourage you to think through what you expect to happen before continuing on:

As with the previous console.log() , this one can be resolved immediately from the Call Stack and Last line to run! will be printed in the Code Output:

Now, we can imagine the 1000 milliseconds have completed and the browser API is now ready to send the callback in to be processed. This looks like the API handing the callback to the Event Queue. Remember, Javascript can only do one thing at a time so if it was still resolving the console.log() on line 7 by the time 1000 milliseconds had gone by, we’d need to ensure any new activity didn’t interrupt any currently running processes. To that end, the callback is handed to the Event Queue to await a time when the Call Stack is completely cleared:

The Event Loop will run when the Call Stack is completely clear to see what’s in the Event Queue:

It sees the console.log() and pulls that into the empty Call Stack:

Now the Call Stack has the capacity to process this console.log() just like the other two:

From there, every line of code has been run and all aspects of the process are cleared. That’s it!

Is that all there is to it?

This guide is meant to give you a high-level overview of what the Event Loop is and how it works. The process has a lot more nuance if you were to spend time digging into it. Once you feel you’ve grasped the basics here, go ahead and see what else you can find!

In this article we have looked at a case where the Call Stack only has one item in it at a time, there are many instances where the Call Stack would have more than one item in it such as instances where a function calls another function. Remember, the item on the top of the Call Stack is processed first even though that is the last item to be added. Feel free to try this out yourself here: Loupe.

What other resources are out there to help me further understand this concept?

There are TONS of great resources out there on the Event Loop. Here are a few that I referenced and found helpful:

Loupe — A tool that will help you visualize how your own code would run through the Event Loop.

JavaScript Event Loop Explained in 5 Minutes — A quick video that is fairly thorough.

What the heck is the event loop anyway? | Philip Roberts | JSConf EU — A longer, more in depth look at the Event Loop (the person giving this talk created the Loupe tool).

',enclosures:[],content_encoded:'

When you execute Javascript on the client, there are some behind-the-scenes processes that make the magic happen. As Javascript is a single-threaded language (it can only do one thing at a time), it needs a way to be told what aspects of your codebase should be run immediately, which should wait on things like API responses and timeouts, and in what order all of that should happen.

Enter, the Event Loop!

The Event Loop takes care of managing the execution of your program in a way that accounts for asynchronous behavior, the nesting of functions, and more.

What does the Event Loop look like?

At a high level, we can think of the Event Loop like this:

Before we step through any code, let’s align on each of these pieces.

The Code Input will simply be the Javascript file you intend to run.

The Call Stack refers to a mechanism that processes each element in your code. It is a stack data structure which is known as first-in-last-out. That means that the first item that is pushed into the Call Stack (as with any stack) will be processed last. It’s like if you went to the library and searched the selves for books you wanted to take home. If you placed each book you found on top of the last, you’d have a pile of books with the first book you took off of the selves on the bottom and the most recent book you found on top. If you handed that pile to the librarian, they would likely take the top book from the pile to check it out for you first. Operating in that manner, they would check out the FIRST book you found on the shelves LAST (after checking all the other books out for you).

The Outside API/Javascript API section refers to an act of using one of the API methods available in the browser (like setTimeout) or the use of another outside API. When requests are made out to APIs, those actions are generally considered asynchronous as they deviate from Javascript running and completing tasks line by line, in order (synchronously).

The Event Queue is a queue data structure similar to a stack, but rather than being first-in-last-out, a queue is first-in-first-out. In terms of a queue, you can think of waiting in a line at the grocery store. If you enter the line first, the cashier will help you to check out before everyone else in the line. If you enter the line last, you’ll have to wait for each customer in front of you to check out, in order, before you’ll be allowed to check out. The first person in the line is the first person to have their groceries scanned and to leave. All programming queues work in this way, including the Event Queue.

The Event Loop is a mechanism that keeps track of whether or not there are any operations needing to be processed in the Event Queue. If there are items that need handling in the queue, it will loop through them and add them to the Call Stack one at a time. The Event Loop can only add items to the Call Stack if the Call Stack is totally empty.

Finally, the Code Output will help us visualize how the Code Input is run in terms of letting us see the order of operations. Consider this depiction to be something like running a file from your browser to see what prints in the console out and when.

How does the Event Loop work?

Now that we have a sense of the different parts of this process and a high-level understanding of each, let’s look at an example to help us understand how a file is read in Javascript and how each line is fed through the Event Loop.

To begin, the first line is a console.log() which is pushed into the Call Stack:

There isn’t any process to wait on here in order for the console.log() to complete, so it’s immediately resolved from the stack and the result is displayed in the Code Output section of this diagram but you can imagine this would be printed to the console of the browser:

With that, the first line is processed and First line to run! is printed out.

Next we’ll look at a setTimeout() function which actually makes use of a timer API available in the browser to keep track of when the milliseconds passed in as the second argument to that function have transpired:

First, the setTimeout() is added to the Call Stack. If you’re unfamiliar, this function takes two arguments: a function to execute and a number to represent the amount of milliseconds to wait until the function passed in as the first argument is called.

The mechanism that keeps track of the time here is a built-in browser API. As that is the case, the next step will be to invoke that timer on the browser itself:

With the timer set to run on the browser, the setTimeout() function is finished being processed in the Call Stack. Next we can look at the console.log() on line 7 while wait for the browser API to return the callback we passed to setTimeout() . I encourage you to think through what you expect to happen before continuing on:

As with the previous console.log() , this one can be resolved immediately from the Call Stack and Last line to run! will be printed in the Code Output:

Now, we can imagine the 1000 milliseconds have completed and the browser API is now ready to send the callback in to be processed. This looks like the API handing the callback to the Event Queue. Remember, Javascript can only do one thing at a time so if it was still resolving the console.log() on line 7 by the time 1000 milliseconds had gone by, we’d need to ensure any new activity didn’t interrupt any currently running processes. To that end, the callback is handed to the Event Queue to await a time when the Call Stack is completely cleared:

The Event Loop will run when the Call Stack is completely clear to see what’s in the Event Queue:

It sees the console.log() and pulls that into the empty Call Stack:

Now the Call Stack has the capacity to process this console.log() just like the other two:

From there, every line of code has been run and all aspects of the process are cleared. That’s it!

Is that all there is to it?

This guide is meant to give you a high-level overview of what the Event Loop is and how it works. The process has a lot more nuance if you were to spend time digging into it. Once you feel you’ve grasped the basics here, go ahead and see what else you can find!

In this article we have looked at a case where the Call Stack only has one item in it at a time, there are many instances where the Call Stack would have more than one item in it such as instances where a function calls another function. Remember, the item on the top of the Call Stack is processed first even though that is the last item to be added. Feel free to try this out yourself here: Loupe.

What other resources are out there to help me further understand this concept?

There are TONS of great resources out there on the Event Loop. Here are a few that I referenced and found helpful:

Loupe — A tool that will help you visualize how your own code would run through the Event Loop.

JavaScript Event Loop Explained in 5 Minutes — A quick video that is fairly thorough.

What the heck is the event loop anyway? | Philip Roberts | JSConf EU — A longer, more in depth look at the Event Loop (the person giving this talk created the Loupe tool).

',media:{}},{id:"https://medium.com/p/2d07585c38a5",title:"A Beginners Guide to Async/Await in Javascript",link:"https://medium.com/@spencer.attick/a-beginners-guide-to-async-await-in-javascript-2d07585c38a5?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:167331047e4,created:167331047e4,category:["asynchronous","coding","code","javascript","computer-science"],content:`
Photo courtesy of Unsplash

What is Async/Await?

Async/await is a concept that arose in the 2017 version of Javascript that allows for asynchronous actions (where one or more things are happening at a time in a program) to be accounted for synchronously (in order). In Javascript, asynchronous behavior generally arises in the form of API requests or manually instantiated Promises. Most other functionality in Javascript is synchronous.

The usage of async/await extends the Promise feature was released into Javascript in 2015. Promises are a solution to nesting callback functions which was how asynchronous behavior was handled before Promises were released. Callbacks (a function passed to another function as an argument) created a problem, often referred to as “callback hell” where functions would be nested to the point of illegibility. Promises allowed for the chaining of functions with the .then() method in order to introduce more readability into a program. Async/await takes things a step further to move away from nesting and chaining into a syntax that looks much more like the synchronous Javascript most folks are used to.

Async/await was developed as “syntactic sugar” on top of Promises and can only be used with a Promise that has been previously created or to handle HTTP responses (which is usually a Promise under the hood).

The async keyword labels a function as containing asynchronous content (a Promise resolution or an HTTP request). The await keyword, indicates which line the program should pause on to wait for the resolution of a Promise.

Here is a simple example:

const promiseExample = new Promise((resolve, reject) => {
const randomBoolean = Math.random() < 0.5;
if (randomBoolean) {
console.log(randomBoolean);
resolve('Boolean was true!')
} else {
console.log(randomBoolean);
reject('Boolean was false!')
}
})

async function asyncExample() {
try {
const promiseOutcome = await promiseExample;
console.log('SUCCESS:', promiseOutcome);
} catch(err) {
console.log('ERROR:', err);
}
}

asyncExample();

Here we have a variable, promiseExample, which takes the result of a Promise. In this case, the randomBoolean variable will randomly resolve to either true or false . If it is true, then the Promise is coded to resolve successfully. If it’s false, then the Promise will reject, or fail.

The asyncExample function is labeled with the async keyword which indicates that it contains asynchronous content. From there, the await keyword tells the program which line to wait on until it receives a value (or an error is thrown).

If an asynchronous action takes places without properly being handled, then you may find that variables that are dependent on the outcome of that asynchronous action are undefined. That happens when the program attempts to read and use those variables before they have been assigned a value by the Promise structure. For that reason, it’s important to properly handle Promises with callbacks, a .then() chain, or by using async/await .

Go ahead and run the above code a few times to see what you get!

When to Use Async/Await

The async/await syntax is ONLY used in conjunction with Promises. It is an optimization that was designed specifically to help make the asynchronous actions that occur when Promises are involved much more readable. Async/await allows you to read code as it will execute, line by line, rather than having to jump around in your code to see what will happen next in sequence.

Advantages of using Async/Await

Async/await allows you to make your code more readable. It moves away from nesting callbacks or chaining .then() methods, both of which can make your code messy and difficult to debug.

The improvement that comes with async/await allows you to run asynchronous code line by line just as you do with synchronous code.

Error Handling

Error handling with async/await look a bit different than if you’re only using the Promise structure. Remember, with Promises, you’ll handle errors with .catch() like this:

doSomething()
.then(() => )
.then(() => )
.catch((err) => )

With async/await , errors are handled with a try/catch block like this:

async function doSomething() {
try {

} catch(err) {

}
}

This syntax will attempt to run your code in the try portion of the block and if ANY of the Promises referenced there with await fail, then they will be caught with the catch section of the block.

Don’t forget the try/catch block! If you do, and your Promise rejects then your code will throw an Unhandled Promise error as you haven’t given the error message an outlet.

Example

Now that we have a decent amount of context, let’s consider a quick example. First we can create a function that will generate a random number:

function generateRandomNum() {
const randomNum = Math.floor(Math.random() * 11);
if (randomNum === 4) {
return 'FOUR';
} else {
return randomNum;
}
}

You may notice that this function contains a twist to keep things interesting. It will return a random number UNLESS that number ends up being 4 in which case it will return a string, FOUR .

Next, we can create a function that returns the result of a Promise which checks the datatype of the number generated by the generateRandomNum() function. This function takes one argument, operandPosition, which will help us narrow in on whether the first or second operand (or number to be added together) is the result of a problem:

function generateNumPromise(operandPosition) {
const randomNum = generateRandomNum();
let result = new Promise((resolve, reject) => {
if (typeof randomNum === 'number') {
resolve(randomNum);
} else {
reject(\`There was a problem! The data type generated for the \${operandPosition} operand was not a number.\`);
}
})
console.log(\`The \${operandPosition} number is \${randomNum}.\`)
return result;
}

This function contains a Promise which will resolve (be considered successful) if randomNum is a number datatype or will reject (be considered erroneous) if randomNum is a string. Either way, a console.log will print out what each operand is each time the program is run for visibility. The result of the Promise will then be returned as result .

From there, we’ll use an async function to await the resolution of each Promise generated by calling generateNumPromise() twice. Remember, async/await is only used when there is a Promise to wait for, so you’ll never need it unless you’ve added a Promise to your code or you’re making an HTTP request as those generally return Promises:

async function addNumbers() {
try {
const firstNum = await generateNumPromise('first');
const secondNum = await generateNumPromise('second');
console.log(\`The sum of firstNum and secondNum = \${firstNum + secondNum}.\`);
} catch (err) {
console.error(err);
}
}

addNumbers();

Here is are a couple runs of the program:

The first demonstrates a case where both operands involved are numbers which allows the program to complete without error.

In the second screenshot, we can see that a string was generated instead of a number which does throw an error, invoking the reject message in the try/catch block.

Key Takeaways

  • async/await always needs to be used with Promises (remember, Node’s fetch method and other API request tools return Promises)
  • don’t forget to use the try/catch block for error handling so you don’t run into issues with unresolved Promises in your code
  • await can only be used in a function that is labeled async

Additional Resources

Synchronous vs. Asynchronous in Javascript

Javascript Promises vs Async Await EXPLAINED (in 5 minutes) video

`,enclosures:[],content_encoded:`
Photo courtesy of Unsplash

What is Async/Await?

Async/await is a concept that arose in the 2017 version of Javascript that allows for asynchronous actions (where one or more things are happening at a time in a program) to be accounted for synchronously (in order). In Javascript, asynchronous behavior generally arises in the form of API requests or manually instantiated Promises. Most other functionality in Javascript is synchronous.

The usage of async/await extends the Promise feature was released into Javascript in 2015. Promises are a solution to nesting callback functions which was how asynchronous behavior was handled before Promises were released. Callbacks (a function passed to another function as an argument) created a problem, often referred to as “callback hell” where functions would be nested to the point of illegibility. Promises allowed for the chaining of functions with the .then() method in order to introduce more readability into a program. Async/await takes things a step further to move away from nesting and chaining into a syntax that looks much more like the synchronous Javascript most folks are used to.

Async/await was developed as “syntactic sugar” on top of Promises and can only be used with a Promise that has been previously created or to handle HTTP responses (which is usually a Promise under the hood).

The async keyword labels a function as containing asynchronous content (a Promise resolution or an HTTP request). The await keyword, indicates which line the program should pause on to wait for the resolution of a Promise.

Here is a simple example:

const promiseExample = new Promise((resolve, reject) => {
const randomBoolean = Math.random() < 0.5;
if (randomBoolean) {
console.log(randomBoolean);
resolve('Boolean was true!')
} else {
console.log(randomBoolean);
reject('Boolean was false!')
}
})

async function asyncExample() {
try {
const promiseOutcome = await promiseExample;
console.log('SUCCESS:', promiseOutcome);
} catch(err) {
console.log('ERROR:', err);
}
}

asyncExample();

Here we have a variable, promiseExample, which takes the result of a Promise. In this case, the randomBoolean variable will randomly resolve to either true or false . If it is true, then the Promise is coded to resolve successfully. If it’s false, then the Promise will reject, or fail.

The asyncExample function is labeled with the async keyword which indicates that it contains asynchronous content. From there, the await keyword tells the program which line to wait on until it receives a value (or an error is thrown).

If an asynchronous action takes places without properly being handled, then you may find that variables that are dependent on the outcome of that asynchronous action are undefined. That happens when the program attempts to read and use those variables before they have been assigned a value by the Promise structure. For that reason, it’s important to properly handle Promises with callbacks, a .then() chain, or by using async/await .

Go ahead and run the above code a few times to see what you get!

When to Use Async/Await

The async/await syntax is ONLY used in conjunction with Promises. It is an optimization that was designed specifically to help make the asynchronous actions that occur when Promises are involved much more readable. Async/await allows you to read code as it will execute, line by line, rather than having to jump around in your code to see what will happen next in sequence.

Advantages of using Async/Await

Async/await allows you to make your code more readable. It moves away from nesting callbacks or chaining .then() methods, both of which can make your code messy and difficult to debug.

The improvement that comes with async/await allows you to run asynchronous code line by line just as you do with synchronous code.

Error Handling

Error handling with async/await look a bit different than if you’re only using the Promise structure. Remember, with Promises, you’ll handle errors with .catch() like this:

doSomething()
.then(() => )
.then(() => )
.catch((err) => )

With async/await , errors are handled with a try/catch block like this:

async function doSomething() {
try {

} catch(err) {

}
}

This syntax will attempt to run your code in the try portion of the block and if ANY of the Promises referenced there with await fail, then they will be caught with the catch section of the block.

Don’t forget the try/catch block! If you do, and your Promise rejects then your code will throw an Unhandled Promise error as you haven’t given the error message an outlet.

Example

Now that we have a decent amount of context, let’s consider a quick example. First we can create a function that will generate a random number:

function generateRandomNum() {
const randomNum = Math.floor(Math.random() * 11);
if (randomNum === 4) {
return 'FOUR';
} else {
return randomNum;
}
}

You may notice that this function contains a twist to keep things interesting. It will return a random number UNLESS that number ends up being 4 in which case it will return a string, FOUR .

Next, we can create a function that returns the result of a Promise which checks the datatype of the number generated by the generateRandomNum() function. This function takes one argument, operandPosition, which will help us narrow in on whether the first or second operand (or number to be added together) is the result of a problem:

function generateNumPromise(operandPosition) {
const randomNum = generateRandomNum();
let result = new Promise((resolve, reject) => {
if (typeof randomNum === 'number') {
resolve(randomNum);
} else {
reject(\`There was a problem! The data type generated for the \${operandPosition} operand was not a number.\`);
}
})
console.log(\`The \${operandPosition} number is \${randomNum}.\`)
return result;
}

This function contains a Promise which will resolve (be considered successful) if randomNum is a number datatype or will reject (be considered erroneous) if randomNum is a string. Either way, a console.log will print out what each operand is each time the program is run for visibility. The result of the Promise will then be returned as result .

From there, we’ll use an async function to await the resolution of each Promise generated by calling generateNumPromise() twice. Remember, async/await is only used when there is a Promise to wait for, so you’ll never need it unless you’ve added a Promise to your code or you’re making an HTTP request as those generally return Promises:

async function addNumbers() {
try {
const firstNum = await generateNumPromise('first');
const secondNum = await generateNumPromise('second');
console.log(\`The sum of firstNum and secondNum = \${firstNum + secondNum}.\`);
} catch (err) {
console.error(err);
}
}

addNumbers();

Here is are a couple runs of the program:

The first demonstrates a case where both operands involved are numbers which allows the program to complete without error.

In the second screenshot, we can see that a string was generated instead of a number which does throw an error, invoking the reject message in the try/catch block.

Key Takeaways

  • async/await always needs to be used with Promises (remember, Node’s fetch method and other API request tools return Promises)
  • don’t forget to use the try/catch block for error handling so you don’t run into issues with unresolved Promises in your code
  • await can only be used in a function that is labeled async

Additional Resources

Synchronous vs. Asynchronous in Javascript

Javascript Promises vs Async Await EXPLAINED (in 5 minutes) video

`,media:{}}],Yh={title:U1,description:G1,link:B1,image:Y1,category:J1,items:$1};function q1(){const[e,t]=we.useState([]),[n,r]=we.useState(null),o=s=>{let l=s.split("<"),u,d;for(const p of l)if(p.includes("cdn-images")){u=p;for(const m of u.split("="))if(m.includes("cdn-images")){d=m.split(" ")[0];break}return d.replace(/"/g,"")}},a=s=>{const l=s.toString().slice(0,-3),d=new Date(l*1e3).toDateString().split(" ");return`${d[1]} ${d[2]} ${d[3]}`},i=s=>{let l=[];for(let u of s){if(l.length===6)return l;u.title.includes("Project")||l.push(u)}return l};return we.useEffect(()=>{async function s(){try{const u=await(await fetch("http://localhost:3000/api/posts")).json();t(i(u))}catch(l){console.log(l),t(i(Yh.items))}}s()},[]),n?de("div",{children:["Error: ",n.message]}):de("div",{id:"blog",children:[A("h2",{children:"Blog"}),A("div",{id:"blog-posts",children:e.length>1?e.map(s=>{const l=o(s.content),u=a(s.created);return A("div",{className:"post",children:de("a",{href:s.id,target:"_blank",children:[A("img",{src:l,alt:s.title}),A("p",{className:"date",children:u}),A("p",{children:s.title})]})},s.id)}):de("div",{children:["It looks like something went wrong. You can check out my blog here: ",A("a",{href:"https://medium.com/@spencer.attick",children:"Spencer's Blog"}),"."]})})]})}const Z1="/assets/SpencerAttickPortfolio-03c16df4.pdf",Q1=[{method:"email",icon:L1,link:"mailto:spencer.attick@gmail"},{method:"linkedin",icon:P1,link:"https://www.linkedin.com/in/ryan-spencer-attick/"},{method:"github",icon:Bh,link:"https://github.com/spencerattick"},{method:"medium",icon:M1,link:"https://medium.com/@spencer.attick"},{method:"resume",icon:H1,link:Z1}];function K1(){return A("div",{id:"contact",children:Q1.map(e=>A("a",{href:e.link,target:"_blank",children:A(Ln,{className:"contactIcon",icon:e.icon})},e.method))})}function X1(){const[e,t]=we.useState([]);we.useState(null);const n=a=>{let i=[];for(let s of a)(s.title.includes("Project:")||s.title.includes("🧑‍💻"))&&i.push(s);return i};we.useEffect(()=>{async function a(){try{const s=await(await fetch("http://localhost:3000/api/posts")).json();t(n(s))}catch(i){console.log(i),t(n(Yh.items))}}a()},[]);const r=a=>{let i=a.split("<"),s,l;for(const u of i)if(u.includes("cdn-images")){s=u;for(const d of s.split("="))if(d.includes("cdn-images")){l=d.split(" ")[0];break}return l.replace(/"/g,"")}},o=a=>a.replace("Project: ","");return de("div",{id:"projects",children:[A("h2",{children:"Projects"}),A("div",{id:"posts-container",children:e.length>0?e.map(a=>{const i=r(a.content),s=o(a.title);return A("div",{className:"post",children:de("a",{href:a.id,target:"_blank",children:[A("img",{src:i,alt:s}),A("p",{children:A("span",{children:s})})]})},a.id)}):de("div",{children:["The server appears to be down at the moment. You can check out my projects here: ",A("a",{href:"https://medium.com/@spencer.attick",children:"Spencer's Blog"}),"."]})})]})}const ey="Spencer's Updates",ty="Recent updates from Spencer",ny="https://www.goodreads.com/user/show/104822881-spencer-attick",ry="https://www.goodreads.com/images/layout/goodreads_logo_144.jpg",oy=[],ay=[{id:"ReadStatus6500933037",title:"Spencer wants to read 'The New Jim Crow: Mass Incarceration in the Age of Colorblindness'",description:` - The New Jim Crow by Michelle Alexander - Spencer wants to read The New Jim Crow: Mass Incarceration in the Age of Colorblindness - by - Michelle Alexander -
- `,link:"https://www.goodreads.com/review/show/4352519893",published:1681150264e3,created:1681150264e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6500925868",title:"Spencer wants to read 'Under a White Sky: The Nature of the Future'",description:` - Under a White Sky by Elizabeth Kolbert - Spencer wants to read Under a White Sky: The Nature of the Future - by - Elizabeth Kolbert -
- `,link:"https://www.goodreads.com/review/show/4839952039",published:1681150116e3,created:1681150116e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6500641760",title:"Spencer wants to read 'How We Get Free: Black Feminism and the Combahee River Collective'",description:` - How We Get Free by Keeanga-Yamahtta Taylor - Spencer wants to read How We Get Free: Black Feminism and the Combahee River Collective - by - Keeanga-Yamahtta Taylor -
- `,link:"https://www.goodreads.com/review/show/5476252974",published:1681144425e3,created:1681144425e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6491139783",title:"Spencer wants to read 'Show Your Work!: 10 Ways to Share Your Creativity and Get Discovered'",description:` - Show Your Work! by Austin Kleon - Spencer wants to read Show Your Work!: 10 Ways to Share Your Creativity and Get Discovered - by - Austin Kleon -
- `,link:"https://www.goodreads.com/review/show/5469503948",published:1680887722e3,created:1680887722e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6478030577",title:"Spencer wants to read 'Walking in the Woods: Go back to nature with the Japanese way of shinrin-yoku'",description:` - Walking in the Woods by Yoshifumi Miyazaki - Spencer wants to read Walking in the Woods: Go back to nature with the Japanese way of shinrin-yoku - by - Yoshifumi Miyazaki -
- `,link:"https://www.goodreads.com/review/show/5460083875",published:1680533426e3,created:1680533426e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6470808816",title:"Spencer wants to read 'The Seven Husbands of Evelyn Hugo'",description:` - The Seven Husbands of Evelyn Hugo by Taylor Jenkins Reid - Spencer wants to read The Seven Husbands of Evelyn Hugo - by - Taylor Jenkins Reid -
- `,link:"https://www.goodreads.com/review/show/5454861866",published:1680347678e3,created:1680347678e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6469804206",title:"Spencer started reading 'The Satanic Verses'",description:` - The Satanic Verses by Salman Rushdie - Spencer started reading The Satanic Verses - by - Salman Rushdie -
- `,link:"https://www.goodreads.com/review/show/5441536242",published:1680312945e3,created:1680312945e3,category:[],enclosures:[],media:{}},{id:"Review5410621036",title:"Spencer added 'Read Dangerously: The Subversive Power of Literature in Troubled Times'",description:` - Read Dangerously by Azar Nafisi - Spencer gave 5 stars to Read Dangerously: The Subversive Power of Literature in Troubled Times (Hardcover) - by - Azar Nafisi -
- - - - `,link:"https://www.goodreads.com/review/show/5410621036",published:1680312945e3,created:1680312945e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6469801096",title:"Spencer has read 'The Phantom Tollbooth'",description:` - The Phantom Tollbooth by Norton Juster - Spencer has read The Phantom Tollbooth - by - Norton Juster -
- `,link:"https://www.goodreads.com/review/show/5454123961",published:1680312866e3,created:1680312866e3,category:[],enclosures:[],media:{}},{id:"ReadStatus6456841873",title:"Spencer wants to read 'Gate of the Sun'",description:` - Gate of the Sun by Elias Khoury - Spencer wants to read Gate of the Sun - by - Elias Khoury -
- `,link:"https://www.goodreads.com/review/show/5444784188",published:1679953095e3,created:1679953095e3,category:[],enclosures:[],media:{}}],iy={title:ey,description:ty,link:ny,image:ry,category:oy,items:ay};function sy(){const[e,t]=we.useState([]),[n,r]=we.useState(null);we.useEffect(()=>{o()},[]);const o=async()=>{try{const d=await(await fetch("http://localhost:3000/api/feed")).json();t(d)}catch(u){console.log(u),t(iy)}},a=u=>{const d=u.toString().slice(0,-3),m=new Date(d*1e3).toDateString().split(" ");return`${m[1]} ${m[2]} ${m[3]}`},i=u=>{let d;return u.title.includes("wants to read")?d=`added to list ${a(u.created)}`:u.title.includes("finished reading")||u.title.includes("has read")||u.title.includes("Spencer added")?d=`finished on ${a(u.created)}`:(u.title.includes("started reading")||u.title.includes("currently reading"))&&(d=`started reading on ${a(u.created)}`),d},s=u=>{const d=u.description,p=d.indexOf("books/")+5,m=d.indexOf(".jpg"),g=d.substring(p,m),v=g.split("/")[1],b=g.split("/")[2].split(".")[0];return`https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/${v}/${b}.jpg`},l=u=>(u=u.substring(u.indexOf("'")+1),u=u.slice(0,-1),u);return n?de("div",{children:["Error: ",n.message]}):de("div",{id:"goodreads",children:[A("h2",{children:"Currently Reading"}),A("div",{id:"goodreads-container",children:e.items?e.items.map((u,d)=>{const p=s(u),m=l(u.title),g=i(u);return A("div",{className:"book-div",children:de("a",{href:u.link,target:"_blank",children:[A("img",{src:p,alt:m}),A("p",{children:g})]})},d)}):de("div",{children:["It looks like something went wrong. You can see what I'm reading here: ",A("a",{href:"https://www.goodreads.com/user/show/104822881-spencer-attick",children:"Goodreads Feed"}),"."]})})]})}function ly(){return de("div",{children:[A(Im,{}),A(Tm,{}),A(X1,{}),A(V1,{}),A(q1,{}),A(sy,{}),A(K1,{})]})}function uy(){return A("div",{id:"loading",children:A("h1",{children:"LOADING..."})})}_o.createRoot(document.getElementById("loading")).render(A(sa.StrictMode,{children:A(uy,{})}));setTimeout(()=>{document.getElementById("loading").remove(),_o.createRoot(document.getElementById("root")).render(A(sa.StrictMode,{children:A(ly,{})}))},1e3); diff --git a/dist/assets/index-5d1ad553.js b/dist/assets/index-5d1ad553.js new file mode 100644 index 0000000..6837d41 --- /dev/null +++ b/dist/assets/index-5d1ad553.js @@ -0,0 +1,866 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))r(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerpolicy&&(o.referrerPolicy=a.referrerpolicy),a.crossorigin==="use-credentials"?o.credentials="include":a.crossorigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(a){if(a.ep)return;a.ep=!0;const o=n(a);fetch(a.href,o)}})();function qh(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Ca={},Qh={get exports(){return Ca},set exports(e){Ca=e}},oo={},se={},Jh={get exports(){return se},set exports(e){se=e}},R={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var zr=Symbol.for("react.element"),Kh=Symbol.for("react.portal"),Xh=Symbol.for("react.fragment"),Zh=Symbol.for("react.strict_mode"),ef=Symbol.for("react.profiler"),tf=Symbol.for("react.provider"),nf=Symbol.for("react.context"),rf=Symbol.for("react.forward_ref"),af=Symbol.for("react.suspense"),of=Symbol.for("react.memo"),sf=Symbol.for("react.lazy"),Sl=Symbol.iterator;function lf(e){return e===null||typeof e!="object"?null:(e=Sl&&e[Sl]||e["@@iterator"],typeof e=="function"?e:null)}var ou={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},iu=Object.assign,su={};function On(e,t,n){this.props=e,this.context=t,this.refs=su,this.updater=n||ou}On.prototype.isReactComponent={};On.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};On.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function lu(){}lu.prototype=On.prototype;function ms(e,t,n){this.props=e,this.context=t,this.refs=su,this.updater=n||ou}var gs=ms.prototype=new lu;gs.constructor=ms;iu(gs,On.prototype);gs.isPureReactComponent=!0;var Il=Array.isArray,cu=Object.prototype.hasOwnProperty,vs={current:null},uu={key:!0,ref:!0,__self:!0,__source:!0};function du(e,t,n){var r,a={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)cu.call(t,r)&&!uu.hasOwnProperty(r)&&(a[r]=t[r]);var s=arguments.length-2;if(s===1)a.children=n;else if(1>>1,ne=_[J];if(0>>1;Ja(Co,z))Lta(Wr,Co)?(_[J]=Wr,_[Lt]=z,J=Lt):(_[J]=Co,_[Ft]=z,J=Ft);else if(Lta(Wr,z))_[J]=Wr,_[Lt]=z,J=Lt;else break e}}return j}function a(_,j){var z=_.sortIndex-j.sortIndex;return z!==0?z:_.id-j.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,s=i.now();e.unstable_now=function(){return i.now()-s}}var l=[],c=[],d=1,p=null,m=3,g=!1,y=!1,b=!1,P=typeof setTimeout=="function"?setTimeout:null,h=typeof clearTimeout=="function"?clearTimeout:null,u=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function f(_){for(var j=n(c);j!==null;){if(j.callback===null)r(c);else if(j.startTime<=_)r(c),j.sortIndex=j.expirationTime,t(l,j);else break;j=n(c)}}function v(_){if(b=!1,f(_),!y)if(n(l)!==null)y=!0,Eo(k);else{var j=n(c);j!==null&&Ao(v,j.startTime-_)}}function k(_,j){y=!1,b&&(b=!1,h(A),A=-1),g=!0;var z=m;try{for(f(j),p=n(l);p!==null&&(!(p.expirationTime>j)||_&&!Fe());){var J=p.callback;if(typeof J=="function"){p.callback=null,m=p.priorityLevel;var ne=J(p.expirationTime<=j);j=e.unstable_now(),typeof ne=="function"?p.callback=ne:p===n(l)&&r(l),f(j)}else r(l);p=n(l)}if(p!==null)var Ur=!0;else{var Ft=n(c);Ft!==null&&Ao(v,Ft.startTime-j),Ur=!1}return Ur}finally{p=null,m=z,g=!1}}var I=!1,T=null,A=-1,L=5,O=-1;function Fe(){return!(e.unstable_now()-O_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):L=0<_?Math.floor(1e3/_):5},e.unstable_getCurrentPriorityLevel=function(){return m},e.unstable_getFirstCallbackNode=function(){return n(l)},e.unstable_next=function(_){switch(m){case 1:case 2:case 3:var j=3;break;default:j=m}var z=m;m=j;try{return _()}finally{m=z}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(_,j){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var z=m;m=_;try{return j()}finally{m=z}},e.unstable_scheduleCallback=function(_,j,z){var J=e.unstable_now();switch(typeof z=="object"&&z!==null?(z=z.delay,z=typeof z=="number"&&0J?(_.sortIndex=z,t(c,_),n(l)===null&&_===n(c)&&(b?(h(A),A=-1):b=!0,Ao(v,z-J))):(_.sortIndex=ne,t(l,_),y||g||(y=!0,Eo(k))),_},e.unstable_shouldYield=Fe,e.unstable_wrapCallback=function(_){var j=m;return function(){var z=m;m=j;try{return _.apply(this,arguments)}finally{m=z}}}})(fu);(function(e){e.exports=fu})(bf);/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var pu=se,_e=li;function w(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ci=Object.prototype.hasOwnProperty,kf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,_l={},El={};function xf(e){return ci.call(El,e)?!0:ci.call(_l,e)?!1:kf.test(e)?El[e]=!0:(_l[e]=!0,!1)}function Sf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function If(e,t,n,r){if(t===null||typeof t>"u"||Sf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ve(e,t,n,r,a,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=a,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var ce={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ce[e]=new ve(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ce[t]=new ve(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ce[e]=new ve(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ce[e]=new ve(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ce[e]=new ve(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ce[e]=new ve(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ce[e]=new ve(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ce[e]=new ve(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ce[e]=new ve(e,5,!1,e.toLowerCase(),null,!1,!1)});var ws=/[\-:]([a-z])/g;function bs(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ws,bs);ce[t]=new ve(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ws,bs);ce[t]=new ve(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ws,bs);ce[t]=new ve(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ce[e]=new ve(e,1,!1,e.toLowerCase(),null,!1,!1)});ce.xlinkHref=new ve("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ce[e]=new ve(e,1,!1,e.toLowerCase(),null,!0,!0)});function ks(e,t,n,r){var a=ce.hasOwnProperty(t)?ce[t]:null;(a!==null?a.type!==0:r||!(2s||a[i]!==o[s]){var l=` +`+a[i].replace(" at new "," at ");return e.displayName&&l.includes("")&&(l=l.replace("",e.displayName)),l}while(1<=i&&0<=s);break}}}finally{No=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Qn(e):""}function Tf(e){switch(e.tag){case 5:return Qn(e.type);case 16:return Qn("Lazy");case 13:return Qn("Suspense");case 19:return Qn("SuspenseList");case 0:case 2:case 15:return e=zo(e.type,!1),e;case 11:return e=zo(e.type.render,!1),e;case 1:return e=zo(e.type,!0),e;default:return""}}function fi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case an:return"Fragment";case rn:return"Portal";case ui:return"Profiler";case xs:return"StrictMode";case di:return"Suspense";case hi:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case vu:return(e.displayName||"Context")+".Consumer";case gu:return(e._context.displayName||"Context")+".Provider";case Ss:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Is:return t=e.displayName||null,t!==null?t:fi(e.type)||"Memo";case ft:t=e._payload,e=e._init;try{return fi(e(t))}catch{}}return null}function _f(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return fi(t);case 8:return t===xs?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function At(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function wu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Ef(e){var t=wu(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var a=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return a.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function $r(e){e._valueTracker||(e._valueTracker=Ef(e))}function bu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=wu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function ja(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function pi(e,t){var n=t.checked;return Y({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Cl(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=At(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ku(e,t){t=t.checked,t!=null&&ks(e,"checked",t,!1)}function mi(e,t){ku(e,t);var n=At(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?gi(e,t.type,n):t.hasOwnProperty("defaultValue")&&gi(e,t.type,At(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Pl(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function gi(e,t,n){(t!=="number"||ja(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Jn=Array.isArray;function wn(e,t,n,r){if(e=e.options,t){t={};for(var a=0;a"+t.valueOf().toString()+"",t=Gr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function hr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var er={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Af=["Webkit","ms","Moz","O"];Object.keys(er).forEach(function(e){Af.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),er[t]=er[e]})});function Tu(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||er.hasOwnProperty(e)&&er[e]?(""+t).trim():t+"px"}function _u(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,a=Tu(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,a):e[n]=a}}var Cf=Y({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function wi(e,t){if(t){if(Cf[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(w(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(w(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(w(61))}if(t.style!=null&&typeof t.style!="object")throw Error(w(62))}}function bi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ki=null;function Ts(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var xi=null,bn=null,kn=null;function zl(e){if(e=Fr(e)){if(typeof xi!="function")throw Error(w(280));var t=e.stateNode;t&&(t=ho(t),xi(e.stateNode,e.type,t))}}function Eu(e){bn?kn?kn.push(e):kn=[e]:bn=e}function Au(){if(bn){var e=bn,t=kn;if(kn=bn=null,zl(e),t)for(e=0;e>>=0,e===0?32:31-(Hf(e)/Uf|0)|0}var Yr=64,qr=4194304;function Kn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ra(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,a=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var s=i&~a;s!==0?r=Kn(s):(o&=i,o!==0&&(r=Kn(o)))}else i=n&~a,i!==0?r=Kn(i):o!==0&&(r=Kn(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&a)&&(a=r&-r,o=t&-t,a>=o||a===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Or(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-We(t),e[t]=n}function $f(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=nr),Wl=String.fromCharCode(32),Vl=!1;function qu(e,t){switch(e){case"keyup":return wp.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Qu(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var on=!1;function kp(e,t){switch(e){case"compositionend":return Qu(t);case"keypress":return t.which!==32?null:(Vl=!0,Wl);case"textInput":return e=t.data,e===Wl&&Vl?null:e;default:return null}}function xp(e,t){if(on)return e==="compositionend"||!zs&&qu(e,t)?(e=Gu(),ya=Ps=vt=null,on=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Yl(n)}}function Zu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Zu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ed(){for(var e=window,t=ja();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=ja(e.document)}return t}function Os(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function jp(e){var t=ed(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Zu(n.ownerDocument.documentElement,n)){if(r!==null&&Os(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var a=n.textContent.length,o=Math.min(r.start,a);r=r.end===void 0?o:Math.min(r.end,a),!e.extend&&o>r&&(a=r,r=o,o=a),a=ql(n,o);var i=ql(n,r);a&&i&&(e.rangeCount!==1||e.anchorNode!==a.node||e.anchorOffset!==a.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(a.node,a.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,sn=null,Ai=null,ar=null,Ci=!1;function Ql(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ci||sn==null||sn!==ja(r)||(r=sn,"selectionStart"in r&&Os(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),ar&&yr(ar,r)||(ar=r,r=Ma(Ai,"onSelect"),0un||(e.current=Ri[un],Ri[un]=null,un--)}function D(e,t){un++,Ri[un]=e.current,e.current=t}var Ct={},fe=Ot(Ct),be=Ot(!1),Yt=Ct;function En(e,t){var n=e.type.contextTypes;if(!n)return Ct;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var a={},o;for(o in n)a[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=a),a}function ke(e){return e=e.childContextTypes,e!=null}function Ha(){W(be),W(fe)}function nc(e,t,n){if(fe.current!==Ct)throw Error(w(168));D(fe,t),D(be,n)}function cd(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var a in r)if(!(a in t))throw Error(w(108,_f(e)||"Unknown",a));return Y({},n,r)}function Ua(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ct,Yt=fe.current,D(fe,e),D(be,be.current),!0}function rc(e,t,n){var r=e.stateNode;if(!r)throw Error(w(169));n?(e=cd(e,t,Yt),r.__reactInternalMemoizedMergedChildContext=e,W(be),W(fe),D(fe,e)):W(be),D(be,n)}var Xe=null,fo=!1,Yo=!1;function ud(e){Xe===null?Xe=[e]:Xe.push(e)}function Vp(e){fo=!0,ud(e)}function Rt(){if(!Yo&&Xe!==null){Yo=!0;var e=0,t=M;try{var n=Xe;for(M=1;e>=i,a-=i,Ze=1<<32-We(t)+a|n<A?(L=T,T=null):L=T.sibling;var O=m(h,T,f[A],v);if(O===null){T===null&&(T=L);break}e&&T&&O.alternate===null&&t(h,T),u=o(O,u,A),I===null?k=O:I.sibling=O,I=O,T=L}if(A===f.length)return n(h,T),B&&Mt(h,A),k;if(T===null){for(;AA?(L=T,T=null):L=T.sibling;var Fe=m(h,T,O.value,v);if(Fe===null){T===null&&(T=L);break}e&&T&&Fe.alternate===null&&t(h,T),u=o(Fe,u,A),I===null?k=Fe:I.sibling=Fe,I=Fe,T=L}if(O.done)return n(h,T),B&&Mt(h,A),k;if(T===null){for(;!O.done;A++,O=f.next())O=p(h,O.value,v),O!==null&&(u=o(O,u,A),I===null?k=O:I.sibling=O,I=O);return B&&Mt(h,A),k}for(T=r(h,T);!O.done;A++,O=f.next())O=g(T,h,A,O.value,v),O!==null&&(e&&O.alternate!==null&&T.delete(O.key===null?A:O.key),u=o(O,u,A),I===null?k=O:I.sibling=O,I=O);return e&&T.forEach(function(Dn){return t(h,Dn)}),B&&Mt(h,A),k}function P(h,u,f,v){if(typeof f=="object"&&f!==null&&f.type===an&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case Br:e:{for(var k=f.key,I=u;I!==null;){if(I.key===k){if(k=f.type,k===an){if(I.tag===7){n(h,I.sibling),u=a(I,f.props.children),u.return=h,h=u;break e}}else if(I.elementType===k||typeof k=="object"&&k!==null&&k.$$typeof===ft&&uc(k)===I.type){n(h,I.sibling),u=a(I,f.props),u.ref=Gn(h,I,f),u.return=h,h=u;break e}n(h,I);break}else t(h,I);I=I.sibling}f.type===an?(u=Gt(f.props.children,h.mode,v,f.key),u.return=h,h=u):(v=_a(f.type,f.key,f.props,null,h.mode,v),v.ref=Gn(h,u,f),v.return=h,h=v)}return i(h);case rn:e:{for(I=f.key;u!==null;){if(u.key===I)if(u.tag===4&&u.stateNode.containerInfo===f.containerInfo&&u.stateNode.implementation===f.implementation){n(h,u.sibling),u=a(u,f.children||[]),u.return=h,h=u;break e}else{n(h,u);break}else t(h,u);u=u.sibling}u=ti(f,h.mode,v),u.return=h,h=u}return i(h);case ft:return I=f._init,P(h,u,I(f._payload),v)}if(Jn(f))return y(h,u,f,v);if(Un(f))return b(h,u,f,v);ta(h,f)}return typeof f=="string"&&f!==""||typeof f=="number"?(f=""+f,u!==null&&u.tag===6?(n(h,u.sibling),u=a(u,f),u.return=h,h=u):(n(h,u),u=ei(f,h.mode,v),u.return=h,h=u),i(h)):n(h,u)}return P}var Cn=yd(!0),wd=yd(!1),Lr={},Je=Ot(Lr),xr=Ot(Lr),Sr=Ot(Lr);function Wt(e){if(e===Lr)throw Error(w(174));return e}function Vs(e,t){switch(D(Sr,t),D(xr,e),D(Je,Lr),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:yi(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=yi(t,e)}W(Je),D(Je,t)}function Pn(){W(Je),W(xr),W(Sr)}function bd(e){Wt(Sr.current);var t=Wt(Je.current),n=yi(t,e.type);t!==n&&(D(xr,e),D(Je,n))}function Bs(e){xr.current===e&&(W(Je),W(xr))}var $=Ot(0);function Ya(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var qo=[];function $s(){for(var e=0;en?n:4,e(!0);var r=Qo.transition;Qo.transition={};try{e(!1),t()}finally{M=n,Qo.transition=r}}function Fd(){return Re().memoizedState}function Yp(e,t,n){var r=_t(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Ld(e))Md(t,n);else if(n=pd(e,t,n,r),n!==null){var a=me();Ve(n,e,r,a),Dd(n,t,r)}}function qp(e,t,n){var r=_t(e),a={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ld(e))Md(t,a);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,s=o(i,n);if(a.hasEagerState=!0,a.eagerState=s,Be(s,i)){var l=t.interleaved;l===null?(a.next=a,Us(t)):(a.next=l.next,l.next=a),t.interleaved=a;return}}catch{}finally{}n=pd(e,t,a,r),n!==null&&(a=me(),Ve(n,e,r,a),Dd(n,t,r))}}function Ld(e){var t=e.alternate;return e===G||t!==null&&t===G}function Md(e,t){or=qa=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Dd(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Es(e,n)}}var Qa={readContext:Oe,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Qp={readContext:Oe,useCallback:function(e,t){return Ge().memoizedState=[e,t===void 0?null:t],e},useContext:Oe,useEffect:hc,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,xa(4194308,4,jd.bind(null,t,e),n)},useLayoutEffect:function(e,t){return xa(4194308,4,e,t)},useInsertionEffect:function(e,t){return xa(4,2,e,t)},useMemo:function(e,t){var n=Ge();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ge();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Yp.bind(null,G,e),[r.memoizedState,e]},useRef:function(e){var t=Ge();return e={current:e},t.memoizedState=e},useState:dc,useDebugValue:Js,useDeferredValue:function(e){return Ge().memoizedState=e},useTransition:function(){var e=dc(!1),t=e[0];return e=Gp.bind(null,e[1]),Ge().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=G,a=Ge();if(B){if(n===void 0)throw Error(w(407));n=n()}else{if(n=t(),ae===null)throw Error(w(349));Qt&30||Sd(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,hc(Td.bind(null,r,o,e),[e]),r.flags|=2048,_r(9,Id.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ge(),t=ae.identifierPrefix;if(B){var n=et,r=Ze;n=(r&~(1<<32-We(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Ir++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Ye]=t,e[kr]=r,qd(e,t,!1,!1),t.stateNode=e;e:{switch(i=bi(n,r),n){case"dialog":H("cancel",e),H("close",e),a=r;break;case"iframe":case"object":case"embed":H("load",e),a=r;break;case"video":case"audio":for(a=0;aNn&&(t.flags|=128,r=!0,Yn(o,!1),t.lanes=4194304)}else{if(!r)if(e=Ya(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Yn(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!B)return de(t),null}else 2*K()-o.renderingStartTime>Nn&&n!==1073741824&&(t.flags|=128,r=!0,Yn(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=K(),t.sibling=null,n=$.current,D($,r?n&1|2:n&1),t):(de(t),null);case 22:case 23:return nl(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Se&1073741824&&(de(t),t.subtreeFlags&6&&(t.flags|=8192)):de(t),null;case 24:return null;case 25:return null}throw Error(w(156,t.tag))}function rm(e,t){switch(Fs(t),t.tag){case 1:return ke(t.type)&&Ha(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Pn(),W(be),W(fe),$s(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Bs(t),null;case 13:if(W($),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(w(340));An()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return W($),null;case 4:return Pn(),null;case 10:return Hs(t.type._context),null;case 22:case 23:return nl(),null;case 24:return null;default:return null}}var ra=!1,he=!1,am=typeof WeakSet=="function"?WeakSet:Set,S=null;function pn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){q(e,t,r)}else n.current=null}function Yi(e,t,n){try{n()}catch(r){q(e,t,r)}}var kc=!1;function om(e,t){if(Pi=Fa,e=ed(),Os(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,s=-1,l=-1,c=0,d=0,p=e,m=null;t:for(;;){for(var g;p!==n||a!==0&&p.nodeType!==3||(s=i+a),p!==o||r!==0&&p.nodeType!==3||(l=i+r),p.nodeType===3&&(i+=p.nodeValue.length),(g=p.firstChild)!==null;)m=p,p=g;for(;;){if(p===e)break t;if(m===n&&++c===a&&(s=i),m===o&&++d===r&&(l=i),(g=p.nextSibling)!==null)break;p=m,m=p.parentNode}p=g}n=s===-1||l===-1?null:{start:s,end:l}}else n=null}n=n||{start:0,end:0}}else n=null;for(ji={focusedElem:e,selectionRange:n},Fa=!1,S=t;S!==null;)if(t=S,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,S=e;else for(;S!==null;){t=S;try{var y=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(y!==null){var b=y.memoizedProps,P=y.memoizedState,h=t.stateNode,u=h.getSnapshotBeforeUpdate(t.elementType===t.type?b:Me(t.type,b),P);h.__reactInternalSnapshotBeforeUpdate=u}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(w(163))}}catch(v){q(t,t.return,v)}if(e=t.sibling,e!==null){e.return=t.return,S=e;break}S=t.return}return y=kc,kc=!1,y}function ir(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var a=r=r.next;do{if((a.tag&e)===e){var o=a.destroy;a.destroy=void 0,o!==void 0&&Yi(t,n,o)}a=a.next}while(a!==r)}}function go(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function qi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Kd(e){var t=e.alternate;t!==null&&(e.alternate=null,Kd(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ye],delete t[kr],delete t[Oi],delete t[Up],delete t[Wp])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Xd(e){return e.tag===5||e.tag===3||e.tag===4}function xc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Xd(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Qi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Da));else if(r!==4&&(e=e.child,e!==null))for(Qi(e,t,n),e=e.sibling;e!==null;)Qi(e,t,n),e=e.sibling}function Ji(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Ji(e,t,n),e=e.sibling;e!==null;)Ji(e,t,n),e=e.sibling}var oe=null,De=!1;function dt(e,t,n){for(n=n.child;n!==null;)Zd(e,t,n),n=n.sibling}function Zd(e,t,n){if(Qe&&typeof Qe.onCommitFiberUnmount=="function")try{Qe.onCommitFiberUnmount(so,n)}catch{}switch(n.tag){case 5:he||pn(n,t);case 6:var r=oe,a=De;oe=null,dt(e,t,n),oe=r,De=a,oe!==null&&(De?(e=oe,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):oe.removeChild(n.stateNode));break;case 18:oe!==null&&(De?(e=oe,n=n.stateNode,e.nodeType===8?Go(e.parentNode,n):e.nodeType===1&&Go(e,n),gr(e)):Go(oe,n.stateNode));break;case 4:r=oe,a=De,oe=n.stateNode.containerInfo,De=!0,dt(e,t,n),oe=r,De=a;break;case 0:case 11:case 14:case 15:if(!he&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){a=r=r.next;do{var o=a,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Yi(n,t,i),a=a.next}while(a!==r)}dt(e,t,n);break;case 1:if(!he&&(pn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(s){q(n,t,s)}dt(e,t,n);break;case 21:dt(e,t,n);break;case 22:n.mode&1?(he=(r=he)||n.memoizedState!==null,dt(e,t,n),he=r):dt(e,t,n);break;default:dt(e,t,n)}}function Sc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new am),t.forEach(function(r){var a=pm.bind(null,e,r);n.has(r)||(n.add(r),r.then(a,a))})}}function Le(e,t){var n=t.deletions;if(n!==null)for(var r=0;ra&&(a=i),r&=~o}if(r=a,r=K()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*sm(r/1960))-r,10e?16:e,yt===null)var r=!1;else{if(e=yt,yt=null,Xa=0,F&6)throw Error(w(331));var a=F;for(F|=4,S=e.current;S!==null;){var o=S,i=o.child;if(S.flags&16){var s=o.deletions;if(s!==null){for(var l=0;lK()-el?$t(e,0):Zs|=n),xe(e,t)}function sh(e,t){t===0&&(e.mode&1?(t=qr,qr<<=1,!(qr&130023424)&&(qr=4194304)):t=1);var n=me();e=at(e,t),e!==null&&(Or(e,t,n),xe(e,n))}function fm(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),sh(e,n)}function pm(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(w(314))}r!==null&&r.delete(t),sh(e,n)}var lh;lh=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||be.current)we=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return we=!1,tm(e,t,n);we=!!(e.flags&131072)}else we=!1,B&&t.flags&1048576&&dd(t,Va,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Sa(e,t),e=t.pendingProps;var a=En(t,fe.current);Sn(t,n),a=Ys(null,t,r,e,a,n);var o=qs();return t.flags|=1,typeof a=="object"&&a!==null&&typeof a.render=="function"&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ke(r)?(o=!0,Ua(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ws(t),a.updater=po,t.stateNode=a,a._reactInternals=t,Hi(t,r,e,n),t=Vi(null,t,r,!0,o,n)):(t.tag=0,B&&o&&Rs(t),pe(null,t,a,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Sa(e,t),e=t.pendingProps,a=r._init,r=a(r._payload),t.type=r,a=t.tag=gm(r),e=Me(r,e),a){case 0:t=Wi(null,t,r,e,n);break e;case 1:t=yc(null,t,r,e,n);break e;case 11:t=gc(null,t,r,e,n);break e;case 14:t=vc(null,t,r,Me(r.type,e),n);break e}throw Error(w(306,r,""))}return t;case 0:return r=t.type,a=t.pendingProps,a=t.elementType===r?a:Me(r,a),Wi(e,t,r,a,n);case 1:return r=t.type,a=t.pendingProps,a=t.elementType===r?a:Me(r,a),yc(e,t,r,a,n);case 3:e:{if($d(t),e===null)throw Error(w(387));r=t.pendingProps,o=t.memoizedState,a=o.element,md(e,t),Ga(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=jn(Error(w(423)),t),t=wc(e,t,r,n,a);break e}else if(r!==a){a=jn(Error(w(424)),t),t=wc(e,t,r,n,a);break e}else for(Ie=St(t.stateNode.containerInfo.firstChild),Te=t,B=!0,He=null,n=wd(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(An(),r===a){t=ot(e,t,n);break e}pe(e,t,r,n)}t=t.child}return t;case 5:return bd(t),e===null&&Li(t),r=t.type,a=t.pendingProps,o=e!==null?e.memoizedProps:null,i=a.children,Ni(r,a)?i=null:o!==null&&Ni(r,o)&&(t.flags|=32),Bd(e,t),pe(e,t,i,n),t.child;case 6:return e===null&&Li(t),null;case 13:return Gd(e,t,n);case 4:return Vs(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Cn(t,null,r,n):pe(e,t,r,n),t.child;case 11:return r=t.type,a=t.pendingProps,a=t.elementType===r?a:Me(r,a),gc(e,t,r,a,n);case 7:return pe(e,t,t.pendingProps,n),t.child;case 8:return pe(e,t,t.pendingProps.children,n),t.child;case 12:return pe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,a=t.pendingProps,o=t.memoizedProps,i=a.value,D(Ba,r._currentValue),r._currentValue=i,o!==null)if(Be(o.value,i)){if(o.children===a.children&&!be.current){t=ot(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var s=o.dependencies;if(s!==null){i=o.child;for(var l=s.firstContext;l!==null;){if(l.context===r){if(o.tag===1){l=tt(-1,n&-n),l.tag=2;var c=o.updateQueue;if(c!==null){c=c.shared;var d=c.pending;d===null?l.next=l:(l.next=d.next,d.next=l),c.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Mi(o.return,n,t),s.lanes|=n;break}l=l.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(w(341));i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),Mi(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}pe(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,r=t.pendingProps.children,Sn(t,n),a=Oe(a),r=r(a),t.flags|=1,pe(e,t,r,n),t.child;case 14:return r=t.type,a=Me(r,t.pendingProps),a=Me(r.type,a),vc(e,t,r,a,n);case 15:return Wd(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,a=t.pendingProps,a=t.elementType===r?a:Me(r,a),Sa(e,t),t.tag=1,ke(r)?(e=!0,Ua(t)):e=!1,Sn(t,n),vd(t,r,a),Hi(t,r,a,n),Vi(null,t,r,!0,e,n);case 19:return Yd(e,t,n);case 22:return Vd(e,t,n)}throw Error(w(156,t.tag))};function ch(e,t){return Ru(e,t)}function mm(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ne(e,t,n,r){return new mm(e,t,n,r)}function al(e){return e=e.prototype,!(!e||!e.isReactComponent)}function gm(e){if(typeof e=="function")return al(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ss)return 11;if(e===Is)return 14}return 2}function Et(e,t){var n=e.alternate;return n===null?(n=Ne(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function _a(e,t,n,r,a,o){var i=2;if(r=e,typeof e=="function")al(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case an:return Gt(n.children,a,o,t);case xs:i=8,a|=8;break;case ui:return e=Ne(12,n,t,a|2),e.elementType=ui,e.lanes=o,e;case di:return e=Ne(13,n,t,a),e.elementType=di,e.lanes=o,e;case hi:return e=Ne(19,n,t,a),e.elementType=hi,e.lanes=o,e;case yu:return yo(n,a,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case gu:i=10;break e;case vu:i=9;break e;case Ss:i=11;break e;case Is:i=14;break e;case ft:i=16,r=null;break e}throw Error(w(130,e==null?e:typeof e,""))}return t=Ne(i,n,t,a),t.elementType=e,t.type=r,t.lanes=o,t}function Gt(e,t,n,r){return e=Ne(7,e,r,t),e.lanes=n,e}function yo(e,t,n,r){return e=Ne(22,e,r,t),e.elementType=yu,e.lanes=n,e.stateNode={isHidden:!1},e}function ei(e,t,n){return e=Ne(6,e,null,t),e.lanes=n,e}function ti(e,t,n){return t=Ne(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function vm(e,t,n,r,a){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ro(0),this.expirationTimes=Ro(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ro(0),this.identifierPrefix=r,this.onRecoverableError=a,this.mutableSourceEagerHydrationData=null}function ol(e,t,n,r,a,o,i,s,l){return e=new vm(e,t,n,s,l),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Ne(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ws(o),e}function ym(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(n){console.error(n)}}t(),e.exports=Ee})(wf);var jc=si;Pa.createRoot=jc.createRoot,Pa.hydrateRoot=jc.hydrateRoot;function Sm(){return ie("div",{id:"intro",title:"Spencer road cycling up a hill near the Pacific Ocean",children:[C("h1",{children:C("span",{className:"underline",children:"SPENCER ATTICK"})}),C("h3",{children:C("span",{className:"underline",children:"he/him"})}),C("p",{children:C("span",{className:"underline",children:"Software Engineer"})})]})}const Im="/assets/aboutMe-255da658.png";function Tm(){return C("div",{id:"about-me",children:ie("p",{children:[C("img",{src:Im,alt:"Spencer wearing sunglasses sitting with his dog - a Doberman Shephard mix"}),"Hi, I'm Spencer! I have a passion for writing great code and solving technical problems. I've worked in the tech industry for over four years now and have learned a ton along the way. Keep scrolling to take a look at what I've been up to!"]})})}function Nc(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),n.push.apply(n,r)}return n}function x(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n-1;a--){var o=n[a],i=(o.tagName||"").toUpperCase();["STYLE","LINK"].indexOf(i)>-1&&(r=o)}return V.head.insertBefore(t,r),e}}var Jm="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";function Nr(){for(var e=12,t="";e-- >0;)t+=Jm[Math.random()*62|0];return t}function Ln(e){for(var t=[],n=(e||[]).length>>>0;n--;)t[n]=e[n];return t}function pl(e){return e.classList?Ln(e.classList):(e.getAttribute("class")||"").split(" ").filter(function(t){return t})}function Ih(e){return"".concat(e).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function Km(e){return Object.keys(e||{}).reduce(function(t,n){return t+"".concat(n,'="').concat(Ih(e[n]),'" ')},"").trim()}function So(e){return Object.keys(e||{}).reduce(function(t,n){return t+"".concat(n,": ").concat(e[n].trim(),";")},"")}function ml(e){return e.size!==qe.size||e.x!==qe.x||e.y!==qe.y||e.rotate!==qe.rotate||e.flipX||e.flipY}function Xm(e){var t=e.transform,n=e.containerWidth,r=e.iconWidth,a={transform:"translate(".concat(n/2," 256)")},o="translate(".concat(t.x*32,", ").concat(t.y*32,") "),i="scale(".concat(t.size/16*(t.flipX?-1:1),", ").concat(t.size/16*(t.flipY?-1:1),") "),s="rotate(".concat(t.rotate," 0 0)"),l={transform:"".concat(o," ").concat(i," ").concat(s)},c={transform:"translate(".concat(r/2*-1," -256)")};return{outer:a,inner:l,path:c}}function Zm(e){var t=e.transform,n=e.width,r=n===void 0?ns:n,a=e.height,o=a===void 0?ns:a,i=e.startCentered,s=i===void 0?!1:i,l="";return s&&vh?l+="translate(".concat(t.x/ht-r/2,"em, ").concat(t.y/ht-o/2,"em) "):s?l+="translate(calc(-50% + ".concat(t.x/ht,"em), calc(-50% + ").concat(t.y/ht,"em)) "):l+="translate(".concat(t.x/ht,"em, ").concat(t.y/ht,"em) "),l+="scale(".concat(t.size/ht*(t.flipX?-1:1),", ").concat(t.size/ht*(t.flipY?-1:1),") "),l+="rotate(".concat(t.rotate,"deg) "),l}var eg=`:root, :host { + --fa-font-solid: normal 900 1em/1 "Font Awesome 6 Solid"; + --fa-font-regular: normal 400 1em/1 "Font Awesome 6 Regular"; + --fa-font-light: normal 300 1em/1 "Font Awesome 6 Light"; + --fa-font-thin: normal 100 1em/1 "Font Awesome 6 Thin"; + --fa-font-duotone: normal 900 1em/1 "Font Awesome 6 Duotone"; + --fa-font-sharp-solid: normal 900 1em/1 "Font Awesome 6 Sharp"; + --fa-font-sharp-regular: normal 400 1em/1 "Font Awesome 6 Sharp"; + --fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands"; +} + +svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa { + overflow: visible; + box-sizing: content-box; +} + +.svg-inline--fa { + display: var(--fa-display, inline-block); + height: 1em; + overflow: visible; + vertical-align: -0.125em; +} +.svg-inline--fa.fa-2xs { + vertical-align: 0.1em; +} +.svg-inline--fa.fa-xs { + vertical-align: 0em; +} +.svg-inline--fa.fa-sm { + vertical-align: -0.0714285705em; +} +.svg-inline--fa.fa-lg { + vertical-align: -0.2em; +} +.svg-inline--fa.fa-xl { + vertical-align: -0.25em; +} +.svg-inline--fa.fa-2xl { + vertical-align: -0.3125em; +} +.svg-inline--fa.fa-pull-left { + margin-right: var(--fa-pull-margin, 0.3em); + width: auto; +} +.svg-inline--fa.fa-pull-right { + margin-left: var(--fa-pull-margin, 0.3em); + width: auto; +} +.svg-inline--fa.fa-li { + width: var(--fa-li-width, 2em); + top: 0.25em; +} +.svg-inline--fa.fa-fw { + width: var(--fa-fw-width, 1.25em); +} + +.fa-layers svg.svg-inline--fa { + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; +} + +.fa-layers-counter, .fa-layers-text { + display: inline-block; + position: absolute; + text-align: center; +} + +.fa-layers { + display: inline-block; + height: 1em; + position: relative; + text-align: center; + vertical-align: -0.125em; + width: 1em; +} +.fa-layers svg.svg-inline--fa { + -webkit-transform-origin: center center; + transform-origin: center center; +} + +.fa-layers-text { + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + -webkit-transform-origin: center center; + transform-origin: center center; +} + +.fa-layers-counter { + background-color: var(--fa-counter-background-color, #ff253a); + border-radius: var(--fa-counter-border-radius, 1em); + box-sizing: border-box; + color: var(--fa-inverse, #fff); + line-height: var(--fa-counter-line-height, 1); + max-width: var(--fa-counter-max-width, 5em); + min-width: var(--fa-counter-min-width, 1.5em); + overflow: hidden; + padding: var(--fa-counter-padding, 0.25em 0.5em); + right: var(--fa-right, 0); + text-overflow: ellipsis; + top: var(--fa-top, 0); + -webkit-transform: scale(var(--fa-counter-scale, 0.25)); + transform: scale(var(--fa-counter-scale, 0.25)); + -webkit-transform-origin: top right; + transform-origin: top right; +} + +.fa-layers-bottom-right { + bottom: var(--fa-bottom, 0); + right: var(--fa-right, 0); + top: auto; + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: bottom right; + transform-origin: bottom right; +} + +.fa-layers-bottom-left { + bottom: var(--fa-bottom, 0); + left: var(--fa-left, 0); + right: auto; + top: auto; + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: bottom left; + transform-origin: bottom left; +} + +.fa-layers-top-right { + top: var(--fa-top, 0); + right: var(--fa-right, 0); + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: top right; + transform-origin: top right; +} + +.fa-layers-top-left { + left: var(--fa-left, 0); + right: auto; + top: var(--fa-top, 0); + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: top left; + transform-origin: top left; +} + +.fa-1x { + font-size: 1em; +} + +.fa-2x { + font-size: 2em; +} + +.fa-3x { + font-size: 3em; +} + +.fa-4x { + font-size: 4em; +} + +.fa-5x { + font-size: 5em; +} + +.fa-6x { + font-size: 6em; +} + +.fa-7x { + font-size: 7em; +} + +.fa-8x { + font-size: 8em; +} + +.fa-9x { + font-size: 9em; +} + +.fa-10x { + font-size: 10em; +} + +.fa-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; +} + +.fa-xs { + font-size: 0.75em; + line-height: 0.0833333337em; + vertical-align: 0.125em; +} + +.fa-sm { + font-size: 0.875em; + line-height: 0.0714285718em; + vertical-align: 0.0535714295em; +} + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; +} + +.fa-xl { + font-size: 1.5em; + line-height: 0.0416666682em; + vertical-align: -0.125em; +} + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; +} + +.fa-fw { + text-align: center; + width: 1.25em; +} + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; +} +.fa-ul > li { + position: relative; +} + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; +} + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); +} + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); +} + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); +} + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); +} + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); +} + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); +} + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-spin-reverse { + --fa-animation-direction: reverse; +} + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); +} + +@media (prefers-reduced-motion: reduce) { + .fa-beat, +.fa-bounce, +.fa-fade, +.fa-beat-fade, +.fa-flip, +.fa-pulse, +.fa-shake, +.fa-spin, +.fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + -webkit-transition-delay: 0s; + transition-delay: 0s; + -webkit-transition-duration: 0s; + transition-duration: 0s; + } +} +@-webkit-keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); + } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); + } +} +@keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); + } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); + } +} +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } +} +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } +} +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); + } +} +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); + } +} +@-webkit-keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); + } +} +@keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); + } +} +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + } +} +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + } +} +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); + } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); + } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); + } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); + } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); + } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); + } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); + } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); + } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); + } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); + } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); + } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); + } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); + } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); + } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); + } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); + } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +.fa-rotate-90 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); +} + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); +} + +.fa-flip-horizontal { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); +} + +.fa-flip-vertical { + -webkit-transform: scale(1, -1); + transform: scale(1, -1); +} + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); +} + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, none)); +} + +.fa-stack { + display: inline-block; + vertical-align: middle; + height: 2em; + position: relative; + width: 2.5em; +} + +.fa-stack-1x, +.fa-stack-2x { + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; + z-index: var(--fa-stack-z-index, auto); +} + +.svg-inline--fa.fa-stack-1x { + height: 1em; + width: 1.25em; +} +.svg-inline--fa.fa-stack-2x { + height: 2em; + width: 2.5em; +} + +.fa-inverse { + color: var(--fa-inverse, #fff); +} + +.sr-only, +.fa-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.svg-inline--fa .fa-primary { + fill: var(--fa-primary-color, currentColor); + opacity: var(--fa-primary-opacity, 1); +} + +.svg-inline--fa .fa-secondary { + fill: var(--fa-secondary-color, currentColor); + opacity: var(--fa-secondary-opacity, 0.4); +} + +.svg-inline--fa.fa-swap-opacity .fa-primary { + opacity: var(--fa-secondary-opacity, 0.4); +} + +.svg-inline--fa.fa-swap-opacity .fa-secondary { + opacity: var(--fa-primary-opacity, 1); +} + +.svg-inline--fa mask .fa-primary, +.svg-inline--fa mask .fa-secondary { + fill: black; +} + +.fad.fa-inverse, +.fa-duotone.fa-inverse { + color: var(--fa-inverse, #fff); +}`;function Th(){var e=yh,t=wh,n=E.cssPrefix,r=E.replacementClass,a=eg;if(n!==e||r!==t){var o=new RegExp("\\.".concat(e,"\\-"),"g"),i=new RegExp("\\--".concat(e,"\\-"),"g"),s=new RegExp("\\.".concat(t),"g");a=a.replace(o,".".concat(n,"-")).replace(i,"--".concat(n,"-")).replace(s,".".concat(r))}return a}var Dc=!1;function ni(){E.autoAddCss&&!Dc&&(Qm(Th()),Dc=!0)}var tg={mixout:function(){return{dom:{css:Th,insertCss:ni}}},hooks:function(){return{beforeDOMElementCreation:function(){ni()},beforeI2svg:function(){ni()}}}},st=Pt||{};st[it]||(st[it]={});st[it].styles||(st[it].styles={});st[it].hooks||(st[it].hooks={});st[it].shims||(st[it].shims=[]);var Ue=st[it],_h=[],ng=function e(){V.removeEventListener("DOMContentLoaded",e),no=1,_h.map(function(t){return t()})},no=!1;ut&&(no=(V.documentElement.doScroll?/^loaded|^c/:/^loaded|^i|^c/).test(V.readyState),no||V.addEventListener("DOMContentLoaded",ng));function rg(e){ut&&(no?setTimeout(e,0):_h.push(e))}function Hr(e){var t=e.tag,n=e.attributes,r=n===void 0?{}:n,a=e.children,o=a===void 0?[]:a;return typeof e=="string"?Ih(e):"<".concat(t," ").concat(Km(r),">").concat(o.map(Hr).join(""),"")}function Hc(e,t,n){if(e&&e[t]&&e[t][n])return{prefix:t,iconName:n,icon:e[t][n]}}var ag=function(t,n){return function(r,a,o,i){return t.call(n,r,a,o,i)}},ri=function(t,n,r,a){var o=Object.keys(t),i=o.length,s=a!==void 0?ag(n,a):n,l,c,d;for(r===void 0?(l=1,d=t[o[0]]):(l=0,d=r);l=55296&&a<=56319&&n=55296&&r<=56319&&n>t+1&&(a=e.charCodeAt(t+1),a>=56320&&a<=57343)?(r-55296)*1024+a-56320+65536:r}function Uc(e){return Object.keys(e).reduce(function(t,n){var r=e[n],a=!!r.icon;return a?t[r.iconName]=r.icon:t[n]=r,t},{})}function os(e,t){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},r=n.skipHooks,a=r===void 0?!1:r,o=Uc(t);typeof Ue.hooks.addPack=="function"&&!a?Ue.hooks.addPack(e,Uc(t)):Ue.styles[e]=x(x({},Ue.styles[e]||{}),o),e==="fas"&&os("fa",t)}var ha,fa,pa,gn=Ue.styles,sg=Ue.shims,lg=(ha={},te(ha,U,Object.values(Pr[U])),te(ha,Q,Object.values(Pr[Q])),ha),gl=null,Eh={},Ah={},Ch={},Ph={},jh={},cg=(fa={},te(fa,U,Object.keys(Ar[U])),te(fa,Q,Object.keys(Ar[Q])),fa);function ug(e){return~Bm.indexOf(e)}function dg(e,t){var n=t.split("-"),r=n[0],a=n.slice(1).join("-");return r===e&&a!==""&&!ug(a)?a:null}var Nh=function(){var t=function(o){return ri(gn,function(i,s,l){return i[l]=ri(s,o,{}),i},{})};Eh=t(function(a,o,i){if(o[3]&&(a[o[3]]=i),o[2]){var s=o[2].filter(function(l){return typeof l=="number"});s.forEach(function(l){a[l.toString(16)]=i})}return a}),Ah=t(function(a,o,i){if(a[i]=i,o[2]){var s=o[2].filter(function(l){return typeof l=="string"});s.forEach(function(l){a[l]=i})}return a}),jh=t(function(a,o,i){var s=o[2];return a[i]=i,s.forEach(function(l){a[l]=i}),a});var n="far"in gn||E.autoFetchSvg,r=ri(sg,function(a,o){var i=o[0],s=o[1],l=o[2];return s==="far"&&!n&&(s="fas"),typeof i=="string"&&(a.names[i]={prefix:s,iconName:l}),typeof i=="number"&&(a.unicodes[i.toString(16)]={prefix:s,iconName:l}),a},{names:{},unicodes:{}});Ch=r.names,Ph=r.unicodes,gl=Io(E.styleDefault,{family:E.familyDefault})};qm(function(e){gl=Io(e.styleDefault,{family:E.familyDefault})});Nh();function vl(e,t){return(Eh[e]||{})[t]}function hg(e,t){return(Ah[e]||{})[t]}function Bt(e,t){return(jh[e]||{})[t]}function zh(e){return Ch[e]||{prefix:null,iconName:null}}function fg(e){var t=Ph[e],n=vl("fas",e);return t||(n?{prefix:"fas",iconName:n}:null)||{prefix:null,iconName:null}}function jt(){return gl}var yl=function(){return{prefix:null,iconName:null,rest:[]}};function Io(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=t.family,r=n===void 0?U:n,a=Ar[r][e],o=Cr[r][e]||Cr[r][a],i=e in Ue.styles?e:null;return o||i||null}var Wc=(pa={},te(pa,U,Object.keys(Pr[U])),te(pa,Q,Object.keys(Pr[Q])),pa);function To(e){var t,n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=n.skipLookups,a=r===void 0?!1:r,o=(t={},te(t,U,"".concat(E.cssPrefix,"-").concat(U)),te(t,Q,"".concat(E.cssPrefix,"-").concat(Q)),t),i=null,s=U;(e.includes(o[U])||e.some(function(c){return Wc[U].includes(c)}))&&(s=U),(e.includes(o[Q])||e.some(function(c){return Wc[Q].includes(c)}))&&(s=Q);var l=e.reduce(function(c,d){var p=dg(E.cssPrefix,d);if(gn[d]?(d=lg[s].includes(d)?Mm[s][d]:d,i=d,c.prefix=d):cg[s].indexOf(d)>-1?(i=d,c.prefix=Io(d,{family:s})):p?c.iconName=p:d!==E.replacementClass&&d!==o[U]&&d!==o[Q]&&c.rest.push(d),!a&&c.prefix&&c.iconName){var m=i==="fa"?zh(c.iconName):{},g=Bt(c.prefix,c.iconName);m.prefix&&(i=null),c.iconName=m.iconName||g||c.iconName,c.prefix=m.prefix||c.prefix,c.prefix==="far"&&!gn.far&&gn.fas&&!E.autoFetchSvg&&(c.prefix="fas")}return c},yl());return(e.includes("fa-brands")||e.includes("fab"))&&(l.prefix="fab"),(e.includes("fa-duotone")||e.includes("fad"))&&(l.prefix="fad"),!l.prefix&&s===Q&&(gn.fass||E.autoFetchSvg)&&(l.prefix="fass",l.iconName=Bt(l.prefix,l.iconName)||l.iconName),(l.prefix==="fa"||i==="fa")&&(l.prefix=jt()||"fas"),l}var pg=function(){function e(){_m(this,e),this.definitions={}}return Em(e,[{key:"add",value:function(){for(var n=this,r=arguments.length,a=new Array(r),o=0;o0&&d.forEach(function(p){typeof p=="string"&&(n[s][p]=c)}),n[s][l]=c}),n}}]),e}(),Vc=[],vn={},Tn={},mg=Object.keys(Tn);function gg(e,t){var n=t.mixoutsTo;return Vc=e,vn={},Object.keys(Tn).forEach(function(r){mg.indexOf(r)===-1&&delete Tn[r]}),Vc.forEach(function(r){var a=r.mixout?r.mixout():{};if(Object.keys(a).forEach(function(i){typeof a[i]=="function"&&(n[i]=a[i]),to(a[i])==="object"&&Object.keys(a[i]).forEach(function(s){n[i]||(n[i]={}),n[i][s]=a[i][s]})}),r.hooks){var o=r.hooks();Object.keys(o).forEach(function(i){vn[i]||(vn[i]=[]),vn[i].push(o[i])})}r.provides&&r.provides(Tn)}),n}function is(e,t){for(var n=arguments.length,r=new Array(n>2?n-2:0),a=2;a1?t-1:0),r=1;r0&&arguments[0]!==void 0?arguments[0]:{};return ut?(Zt("beforeI2svg",t),lt("pseudoElements2svg",t),lt("i2svg",t)):Promise.reject("Operation requires a DOM of some kind.")},watch:function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},n=t.autoReplaceSvgRoot;E.autoReplaceSvg===!1&&(E.autoReplaceSvg=!0),E.observeMutations=!0,rg(function(){bg({autoReplaceSvgRoot:n}),Zt("watch",t)})}},wg={icon:function(t){if(t===null)return null;if(to(t)==="object"&&t.prefix&&t.iconName)return{prefix:t.prefix,iconName:Bt(t.prefix,t.iconName)||t.iconName};if(Array.isArray(t)&&t.length===2){var n=t[1].indexOf("fa-")===0?t[1].slice(3):t[1],r=Io(t[0]);return{prefix:r,iconName:Bt(r,n)||n}}if(typeof t=="string"&&(t.indexOf("".concat(E.cssPrefix,"-"))>-1||t.match(Dm))){var a=To(t.split(" "),{skipLookups:!0});return{prefix:a.prefix||jt(),iconName:Bt(a.prefix,a.iconName)||a.iconName}}if(typeof t=="string"){var o=jt();return{prefix:o,iconName:Bt(o,t)||t}}}},Ce={noAuto:vg,config:E,dom:yg,parse:wg,library:Oh,findIconDefinition:ss,toHtml:Hr},bg=function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},n=t.autoReplaceSvgRoot,r=n===void 0?V:n;(Object.keys(Ue.styles).length>0||E.autoFetchSvg)&&ut&&E.autoReplaceSvg&&Ce.dom.i2svg({node:r})};function _o(e,t){return Object.defineProperty(e,"abstract",{get:t}),Object.defineProperty(e,"html",{get:function(){return e.abstract.map(function(r){return Hr(r)})}}),Object.defineProperty(e,"node",{get:function(){if(ut){var r=V.createElement("div");return r.innerHTML=e.html,r.children}}}),e}function kg(e){var t=e.children,n=e.main,r=e.mask,a=e.attributes,o=e.styles,i=e.transform;if(ml(i)&&n.found&&!r.found){var s=n.width,l=n.height,c={x:s/l/2,y:.5};a.style=So(x(x({},o),{},{"transform-origin":"".concat(c.x+i.x/16,"em ").concat(c.y+i.y/16,"em")}))}return[{tag:"svg",attributes:a,children:t}]}function xg(e){var t=e.prefix,n=e.iconName,r=e.children,a=e.attributes,o=e.symbol,i=o===!0?"".concat(t,"-").concat(E.cssPrefix,"-").concat(n):o;return[{tag:"svg",attributes:{style:"display: none;"},children:[{tag:"symbol",attributes:x(x({},a),{},{id:i}),children:r}]}]}function wl(e){var t=e.icons,n=t.main,r=t.mask,a=e.prefix,o=e.iconName,i=e.transform,s=e.symbol,l=e.title,c=e.maskId,d=e.titleId,p=e.extra,m=e.watchable,g=m===void 0?!1:m,y=r.found?r:n,b=y.width,P=y.height,h=a==="fak",u=[E.replacementClass,o?"".concat(E.cssPrefix,"-").concat(o):""].filter(function(L){return p.classes.indexOf(L)===-1}).filter(function(L){return L!==""||!!L}).concat(p.classes).join(" "),f={children:[],attributes:x(x({},p.attributes),{},{"data-prefix":a,"data-icon":o,class:u,role:p.attributes.role||"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 ".concat(b," ").concat(P)})},v=h&&!~p.classes.indexOf("fa-fw")?{width:"".concat(b/P*16*.0625,"em")}:{};g&&(f.attributes[Xt]=""),l&&(f.children.push({tag:"title",attributes:{id:f.attributes["aria-labelledby"]||"title-".concat(d||Nr())},children:[l]}),delete f.attributes.title);var k=x(x({},f),{},{prefix:a,iconName:o,main:n,mask:r,maskId:c,transform:i,symbol:s,styles:x(x({},v),p.styles)}),I=r.found&&n.found?lt("generateAbstractMask",k)||{children:[],attributes:{}}:lt("generateAbstractIcon",k)||{children:[],attributes:{}},T=I.children,A=I.attributes;return k.children=T,k.attributes=A,s?xg(k):kg(k)}function Bc(e){var t=e.content,n=e.width,r=e.height,a=e.transform,o=e.title,i=e.extra,s=e.watchable,l=s===void 0?!1:s,c=x(x(x({},i.attributes),o?{title:o}:{}),{},{class:i.classes.join(" ")});l&&(c[Xt]="");var d=x({},i.styles);ml(a)&&(d.transform=Zm({transform:a,startCentered:!0,width:n,height:r}),d["-webkit-transform"]=d.transform);var p=So(d);p.length>0&&(c.style=p);var m=[];return m.push({tag:"span",attributes:c,children:[t]}),o&&m.push({tag:"span",attributes:{class:"sr-only"},children:[o]}),m}function Sg(e){var t=e.content,n=e.title,r=e.extra,a=x(x(x({},r.attributes),n?{title:n}:{}),{},{class:r.classes.join(" ")}),o=So(r.styles);o.length>0&&(a.style=o);var i=[];return i.push({tag:"span",attributes:a,children:[t]}),n&&i.push({tag:"span",attributes:{class:"sr-only"},children:[n]}),i}var ai=Ue.styles;function ls(e){var t=e[0],n=e[1],r=e.slice(4),a=cl(r,1),o=a[0],i=null;return Array.isArray(o)?i={tag:"g",attributes:{class:"".concat(E.cssPrefix,"-").concat(Vt.GROUP)},children:[{tag:"path",attributes:{class:"".concat(E.cssPrefix,"-").concat(Vt.SECONDARY),fill:"currentColor",d:o[0]}},{tag:"path",attributes:{class:"".concat(E.cssPrefix,"-").concat(Vt.PRIMARY),fill:"currentColor",d:o[1]}}]}:i={tag:"path",attributes:{fill:"currentColor",d:o}},{found:!0,width:t,height:n,icon:i}}var Ig={found:!1,width:512,height:512};function Tg(e,t){!bh&&!E.showMissingIcons&&e&&console.error('Icon with name "'.concat(e,'" and prefix "').concat(t,'" is missing.'))}function cs(e,t){var n=t;return t==="fa"&&E.styleDefault!==null&&(t=jt()),new Promise(function(r,a){if(lt("missingIconAbstract"),n==="fa"){var o=zh(e)||{};e=o.iconName||e,t=o.prefix||t}if(e&&t&&ai[t]&&ai[t][e]){var i=ai[t][e];return r(ls(i))}Tg(e,t),r(x(x({},Ig),{},{icon:E.showMissingIcons&&e?lt("missingIconAbstract")||{}:{}}))})}var $c=function(){},us=E.measurePerformance&&ia&&ia.mark&&ia.measure?ia:{mark:$c,measure:$c},Zn='FA "6.3.0"',_g=function(t){return us.mark("".concat(Zn," ").concat(t," begins")),function(){return Rh(t)}},Rh=function(t){us.mark("".concat(Zn," ").concat(t," ends")),us.measure("".concat(Zn," ").concat(t),"".concat(Zn," ").concat(t," begins"),"".concat(Zn," ").concat(t," ends"))},bl={begin:_g,end:Rh},Ea=function(){};function Gc(e){var t=e.getAttribute?e.getAttribute(Xt):null;return typeof t=="string"}function Eg(e){var t=e.getAttribute?e.getAttribute(dl):null,n=e.getAttribute?e.getAttribute(hl):null;return t&&n}function Ag(e){return e&&e.classList&&e.classList.contains&&e.classList.contains(E.replacementClass)}function Cg(){if(E.autoReplaceSvg===!0)return Aa.replace;var e=Aa[E.autoReplaceSvg];return e||Aa.replace}function Pg(e){return V.createElementNS("http://www.w3.org/2000/svg",e)}function jg(e){return V.createElement(e)}function Fh(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=t.ceFn,r=n===void 0?e.tag==="svg"?Pg:jg:n;if(typeof e=="string")return V.createTextNode(e);var a=r(e.tag);Object.keys(e.attributes||[]).forEach(function(i){a.setAttribute(i,e.attributes[i])});var o=e.children||[];return o.forEach(function(i){a.appendChild(Fh(i,{ceFn:r}))}),a}function Ng(e){var t=" ".concat(e.outerHTML," ");return t="".concat(t,"Font Awesome fontawesome.com "),t}var Aa={replace:function(t){var n=t[0];if(n.parentNode)if(t[1].forEach(function(a){n.parentNode.insertBefore(Fh(a),n)}),n.getAttribute(Xt)===null&&E.keepOriginalSource){var r=V.createComment(Ng(n));n.parentNode.replaceChild(r,n)}else n.remove()},nest:function(t){var n=t[0],r=t[1];if(~pl(n).indexOf(E.replacementClass))return Aa.replace(t);var a=new RegExp("".concat(E.cssPrefix,"-.*"));if(delete r[0].attributes.id,r[0].attributes.class){var o=r[0].attributes.class.split(" ").reduce(function(s,l){return l===E.replacementClass||l.match(a)?s.toSvg.push(l):s.toNode.push(l),s},{toNode:[],toSvg:[]});r[0].attributes.class=o.toSvg.join(" "),o.toNode.length===0?n.removeAttribute("class"):n.setAttribute("class",o.toNode.join(" "))}var i=r.map(function(s){return Hr(s)}).join(` +`);n.setAttribute(Xt,""),n.innerHTML=i}};function Yc(e){e()}function Lh(e,t){var n=typeof t=="function"?t:Ea;if(e.length===0)n();else{var r=Yc;E.mutateApproach===Fm&&(r=Pt.requestAnimationFrame||Yc),r(function(){var a=Cg(),o=bl.begin("mutate");e.map(a),o(),n()})}}var kl=!1;function Mh(){kl=!0}function ds(){kl=!1}var ro=null;function qc(e){if(Lc&&E.observeMutations){var t=e.treeCallback,n=t===void 0?Ea:t,r=e.nodeCallback,a=r===void 0?Ea:r,o=e.pseudoElementsCallback,i=o===void 0?Ea:o,s=e.observeMutationsRoot,l=s===void 0?V:s;ro=new Lc(function(c){if(!kl){var d=jt();Ln(c).forEach(function(p){if(p.type==="childList"&&p.addedNodes.length>0&&!Gc(p.addedNodes[0])&&(E.searchPseudoElements&&i(p.target),n(p.target)),p.type==="attributes"&&p.target.parentNode&&E.searchPseudoElements&&i(p.target.parentNode),p.type==="attributes"&&Gc(p.target)&&~Vm.indexOf(p.attributeName))if(p.attributeName==="class"&&Eg(p.target)){var m=To(pl(p.target)),g=m.prefix,y=m.iconName;p.target.setAttribute(dl,g||d),y&&p.target.setAttribute(hl,y)}else Ag(p.target)&&a(p.target)})}}),ut&&ro.observe(l,{childList:!0,attributes:!0,characterData:!0,subtree:!0})}}function zg(){ro&&ro.disconnect()}function Og(e){var t=e.getAttribute("style"),n=[];return t&&(n=t.split(";").reduce(function(r,a){var o=a.split(":"),i=o[0],s=o.slice(1);return i&&s.length>0&&(r[i]=s.join(":").trim()),r},{})),n}function Rg(e){var t=e.getAttribute("data-prefix"),n=e.getAttribute("data-icon"),r=e.innerText!==void 0?e.innerText.trim():"",a=To(pl(e));return a.prefix||(a.prefix=jt()),t&&n&&(a.prefix=t,a.iconName=n),a.iconName&&a.prefix||(a.prefix&&r.length>0&&(a.iconName=hg(a.prefix,e.innerText)||vl(a.prefix,as(e.innerText))),!a.iconName&&E.autoFetchSvg&&e.firstChild&&e.firstChild.nodeType===Node.TEXT_NODE&&(a.iconName=e.firstChild.data)),a}function Fg(e){var t=Ln(e.attributes).reduce(function(a,o){return a.name!=="class"&&a.name!=="style"&&(a[o.name]=o.value),a},{}),n=e.getAttribute("title"),r=e.getAttribute("data-fa-title-id");return E.autoA11y&&(n?t["aria-labelledby"]="".concat(E.replacementClass,"-title-").concat(r||Nr()):(t["aria-hidden"]="true",t.focusable="false")),t}function Lg(){return{iconName:null,title:null,titleId:null,prefix:null,transform:qe,symbol:!1,mask:{iconName:null,prefix:null,rest:[]},maskId:null,extra:{classes:[],styles:{},attributes:{}}}}function Qc(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{styleParser:!0},n=Rg(e),r=n.iconName,a=n.prefix,o=n.rest,i=Fg(e),s=is("parseNodeAttributes",{},e),l=t.styleParser?Og(e):[];return x({iconName:r,title:e.getAttribute("title"),titleId:e.getAttribute("data-fa-title-id"),prefix:a,transform:qe,mask:{iconName:null,prefix:null,rest:[]},maskId:null,symbol:!1,extra:{classes:o,styles:l,attributes:i}},s)}var Mg=Ue.styles;function Dh(e){var t=E.autoReplaceSvg==="nest"?Qc(e,{styleParser:!1}):Qc(e);return~t.extra.classes.indexOf(kh)?lt("generateLayersText",e,t):lt("generateSvgReplacementMutation",e,t)}var Nt=new Set;fl.map(function(e){Nt.add("fa-".concat(e))});Object.keys(Ar[U]).map(Nt.add.bind(Nt));Object.keys(Ar[Q]).map(Nt.add.bind(Nt));Nt=Mr(Nt);function Jc(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;if(!ut)return Promise.resolve();var n=V.documentElement.classList,r=function(p){return n.add("".concat(Mc,"-").concat(p))},a=function(p){return n.remove("".concat(Mc,"-").concat(p))},o=E.autoFetchSvg?Nt:fl.map(function(d){return"fa-".concat(d)}).concat(Object.keys(Mg));o.includes("fa")||o.push("fa");var i=[".".concat(kh,":not([").concat(Xt,"])")].concat(o.map(function(d){return".".concat(d,":not([").concat(Xt,"])")})).join(", ");if(i.length===0)return Promise.resolve();var s=[];try{s=Ln(e.querySelectorAll(i))}catch{}if(s.length>0)r("pending"),a("complete");else return Promise.resolve();var l=bl.begin("onTree"),c=s.reduce(function(d,p){try{var m=Dh(p);m&&d.push(m)}catch(g){bh||g.name==="MissingIcon"&&console.error(g)}return d},[]);return new Promise(function(d,p){Promise.all(c).then(function(m){Lh(m,function(){r("active"),r("complete"),a("pending"),typeof t=="function"&&t(),l(),d()})}).catch(function(m){l(),p(m)})})}function Dg(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;Dh(e).then(function(n){n&&Lh([n],t)})}function Hg(e){return function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=(t||{}).icon?t:ss(t||{}),a=n.mask;return a&&(a=(a||{}).icon?a:ss(a||{})),e(r,x(x({},n),{},{mask:a}))}}var Ug=function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=n.transform,a=r===void 0?qe:r,o=n.symbol,i=o===void 0?!1:o,s=n.mask,l=s===void 0?null:s,c=n.maskId,d=c===void 0?null:c,p=n.title,m=p===void 0?null:p,g=n.titleId,y=g===void 0?null:g,b=n.classes,P=b===void 0?[]:b,h=n.attributes,u=h===void 0?{}:h,f=n.styles,v=f===void 0?{}:f;if(t){var k=t.prefix,I=t.iconName,T=t.icon;return _o(x({type:"icon"},t),function(){return Zt("beforeDOMElementCreation",{iconDefinition:t,params:n}),E.autoA11y&&(m?u["aria-labelledby"]="".concat(E.replacementClass,"-title-").concat(y||Nr()):(u["aria-hidden"]="true",u.focusable="false")),wl({icons:{main:ls(T),mask:l?ls(l.icon):{found:!1,width:null,height:null,icon:{}}},prefix:k,iconName:I,transform:x(x({},qe),a),symbol:i,title:m,maskId:d,titleId:y,extra:{attributes:u,styles:v,classes:P}})})}},Wg={mixout:function(){return{icon:Hg(Ug)}},hooks:function(){return{mutationObserverCallbacks:function(n){return n.treeCallback=Jc,n.nodeCallback=Dg,n}}},provides:function(t){t.i2svg=function(n){var r=n.node,a=r===void 0?V:r,o=n.callback,i=o===void 0?function(){}:o;return Jc(a,i)},t.generateSvgReplacementMutation=function(n,r){var a=r.iconName,o=r.title,i=r.titleId,s=r.prefix,l=r.transform,c=r.symbol,d=r.mask,p=r.maskId,m=r.extra;return new Promise(function(g,y){Promise.all([cs(a,s),d.iconName?cs(d.iconName,d.prefix):Promise.resolve({found:!1,width:512,height:512,icon:{}})]).then(function(b){var P=cl(b,2),h=P[0],u=P[1];g([n,wl({icons:{main:h,mask:u},prefix:s,iconName:a,transform:l,symbol:c,maskId:p,title:o,titleId:i,extra:m,watchable:!0})])}).catch(y)})},t.generateAbstractIcon=function(n){var r=n.children,a=n.attributes,o=n.main,i=n.transform,s=n.styles,l=So(s);l.length>0&&(a.style=l);var c;return ml(i)&&(c=lt("generateAbstractTransformGrouping",{main:o,transform:i,containerWidth:o.width,iconWidth:o.width})),r.push(c||o.icon),{children:r,attributes:a}}}},Vg={mixout:function(){return{layer:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=r.classes,o=a===void 0?[]:a;return _o({type:"layer"},function(){Zt("beforeDOMElementCreation",{assembler:n,params:r});var i=[];return n(function(s){Array.isArray(s)?s.map(function(l){i=i.concat(l.abstract)}):i=i.concat(s.abstract)}),[{tag:"span",attributes:{class:["".concat(E.cssPrefix,"-layers")].concat(Mr(o)).join(" ")},children:i}]})}}}},Bg={mixout:function(){return{counter:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=r.title,o=a===void 0?null:a,i=r.classes,s=i===void 0?[]:i,l=r.attributes,c=l===void 0?{}:l,d=r.styles,p=d===void 0?{}:d;return _o({type:"counter",content:n},function(){return Zt("beforeDOMElementCreation",{content:n,params:r}),Sg({content:n.toString(),title:o,extra:{attributes:c,styles:p,classes:["".concat(E.cssPrefix,"-layers-counter")].concat(Mr(s))}})})}}}},$g={mixout:function(){return{text:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=r.transform,o=a===void 0?qe:a,i=r.title,s=i===void 0?null:i,l=r.classes,c=l===void 0?[]:l,d=r.attributes,p=d===void 0?{}:d,m=r.styles,g=m===void 0?{}:m;return _o({type:"text",content:n},function(){return Zt("beforeDOMElementCreation",{content:n,params:r}),Bc({content:n,transform:x(x({},qe),o),title:s,extra:{attributes:p,styles:g,classes:["".concat(E.cssPrefix,"-layers-text")].concat(Mr(c))}})})}}},provides:function(t){t.generateLayersText=function(n,r){var a=r.title,o=r.transform,i=r.extra,s=null,l=null;if(vh){var c=parseInt(getComputedStyle(n).fontSize,10),d=n.getBoundingClientRect();s=d.width/c,l=d.height/c}return E.autoA11y&&!a&&(i.attributes["aria-hidden"]="true"),Promise.resolve([n,Bc({content:n.innerHTML,width:s,height:l,transform:o,title:a,extra:i,watchable:!0})])}}},Gg=new RegExp('"',"ug"),Kc=[1105920,1112319];function Yg(e){var t=e.replace(Gg,""),n=ig(t,0),r=n>=Kc[0]&&n<=Kc[1],a=t.length===2?t[0]===t[1]:!1;return{value:as(a?t[0]:t),isSecondary:r||a}}function Xc(e,t){var n="".concat(Rm).concat(t.replace(":","-"));return new Promise(function(r,a){if(e.getAttribute(n)!==null)return r();var o=Ln(e.children),i=o.filter(function(T){return T.getAttribute(rs)===t})[0],s=Pt.getComputedStyle(e,t),l=s.getPropertyValue("font-family").match(Hm),c=s.getPropertyValue("font-weight"),d=s.getPropertyValue("content");if(i&&!l)return e.removeChild(i),r();if(l&&d!=="none"&&d!==""){var p=s.getPropertyValue("content"),m=~["Sharp"].indexOf(l[2])?Q:U,g=~["Solid","Regular","Light","Thin","Duotone","Brands","Kit"].indexOf(l[2])?Cr[m][l[2].toLowerCase()]:Um[m][c],y=Yg(p),b=y.value,P=y.isSecondary,h=l[0].startsWith("FontAwesome"),u=vl(g,b),f=u;if(h){var v=fg(b);v.iconName&&v.prefix&&(u=v.iconName,g=v.prefix)}if(u&&!P&&(!i||i.getAttribute(dl)!==g||i.getAttribute(hl)!==f)){e.setAttribute(n,f),i&&e.removeChild(i);var k=Lg(),I=k.extra;I.attributes[rs]=t,cs(u,g).then(function(T){var A=wl(x(x({},k),{},{icons:{main:T,mask:yl()},prefix:g,iconName:f,extra:I,watchable:!0})),L=V.createElement("svg");t==="::before"?e.insertBefore(L,e.firstChild):e.appendChild(L),L.outerHTML=A.map(function(O){return Hr(O)}).join(` +`),e.removeAttribute(n),r()}).catch(a)}else r()}else r()})}function qg(e){return Promise.all([Xc(e,"::before"),Xc(e,"::after")])}function Qg(e){return e.parentNode!==document.head&&!~Lm.indexOf(e.tagName.toUpperCase())&&!e.getAttribute(rs)&&(!e.parentNode||e.parentNode.tagName!=="svg")}function Zc(e){if(ut)return new Promise(function(t,n){var r=Ln(e.querySelectorAll("*")).filter(Qg).map(qg),a=bl.begin("searchPseudoElements");Mh(),Promise.all(r).then(function(){a(),ds(),t()}).catch(function(){a(),ds(),n()})})}var Jg={hooks:function(){return{mutationObserverCallbacks:function(n){return n.pseudoElementsCallback=Zc,n}}},provides:function(t){t.pseudoElements2svg=function(n){var r=n.node,a=r===void 0?V:r;E.searchPseudoElements&&Zc(a)}}},eu=!1,Kg={mixout:function(){return{dom:{unwatch:function(){Mh(),eu=!0}}}},hooks:function(){return{bootstrap:function(){qc(is("mutationObserverCallbacks",{}))},noAuto:function(){zg()},watch:function(n){var r=n.observeMutationsRoot;eu?ds():qc(is("mutationObserverCallbacks",{observeMutationsRoot:r}))}}}},tu=function(t){var n={size:16,x:0,y:0,flipX:!1,flipY:!1,rotate:0};return t.toLowerCase().split(" ").reduce(function(r,a){var o=a.toLowerCase().split("-"),i=o[0],s=o.slice(1).join("-");if(i&&s==="h")return r.flipX=!0,r;if(i&&s==="v")return r.flipY=!0,r;if(s=parseFloat(s),isNaN(s))return r;switch(i){case"grow":r.size=r.size+s;break;case"shrink":r.size=r.size-s;break;case"left":r.x=r.x-s;break;case"right":r.x=r.x+s;break;case"up":r.y=r.y-s;break;case"down":r.y=r.y+s;break;case"rotate":r.rotate=r.rotate+s;break}return r},n)},Xg={mixout:function(){return{parse:{transform:function(n){return tu(n)}}}},hooks:function(){return{parseNodeAttributes:function(n,r){var a=r.getAttribute("data-fa-transform");return a&&(n.transform=tu(a)),n}}},provides:function(t){t.generateAbstractTransformGrouping=function(n){var r=n.main,a=n.transform,o=n.containerWidth,i=n.iconWidth,s={transform:"translate(".concat(o/2," 256)")},l="translate(".concat(a.x*32,", ").concat(a.y*32,") "),c="scale(".concat(a.size/16*(a.flipX?-1:1),", ").concat(a.size/16*(a.flipY?-1:1),") "),d="rotate(".concat(a.rotate," 0 0)"),p={transform:"".concat(l," ").concat(c," ").concat(d)},m={transform:"translate(".concat(i/2*-1," -256)")},g={outer:s,inner:p,path:m};return{tag:"g",attributes:x({},g.outer),children:[{tag:"g",attributes:x({},g.inner),children:[{tag:r.icon.tag,children:r.icon.children,attributes:x(x({},r.icon.attributes),g.path)}]}]}}}},oi={x:0,y:0,width:"100%",height:"100%"};function nu(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return e.attributes&&(e.attributes.fill||t)&&(e.attributes.fill="black"),e}function Zg(e){return e.tag==="g"?e.children:[e]}var e1={hooks:function(){return{parseNodeAttributes:function(n,r){var a=r.getAttribute("data-fa-mask"),o=a?To(a.split(" ").map(function(i){return i.trim()})):yl();return o.prefix||(o.prefix=jt()),n.mask=o,n.maskId=r.getAttribute("data-fa-mask-id"),n}}},provides:function(t){t.generateAbstractMask=function(n){var r=n.children,a=n.attributes,o=n.main,i=n.mask,s=n.maskId,l=n.transform,c=o.width,d=o.icon,p=i.width,m=i.icon,g=Xm({transform:l,containerWidth:p,iconWidth:c}),y={tag:"rect",attributes:x(x({},oi),{},{fill:"white"})},b=d.children?{children:d.children.map(nu)}:{},P={tag:"g",attributes:x({},g.inner),children:[nu(x({tag:d.tag,attributes:x(x({},d.attributes),g.path)},b))]},h={tag:"g",attributes:x({},g.outer),children:[P]},u="mask-".concat(s||Nr()),f="clip-".concat(s||Nr()),v={tag:"mask",attributes:x(x({},oi),{},{id:u,maskUnits:"userSpaceOnUse",maskContentUnits:"userSpaceOnUse"}),children:[y,h]},k={tag:"defs",children:[{tag:"clipPath",attributes:{id:f},children:Zg(m)},v]};return r.push(k,{tag:"rect",attributes:x({fill:"currentColor","clip-path":"url(#".concat(f,")"),mask:"url(#".concat(u,")")},oi)}),{children:r,attributes:a}}}},t1={provides:function(t){var n=!1;Pt.matchMedia&&(n=Pt.matchMedia("(prefers-reduced-motion: reduce)").matches),t.missingIconAbstract=function(){var r=[],a={fill:"currentColor"},o={attributeType:"XML",repeatCount:"indefinite",dur:"2s"};r.push({tag:"path",attributes:x(x({},a),{},{d:"M156.5,447.7l-12.6,29.5c-18.7-9.5-35.9-21.2-51.5-34.9l22.7-22.7C127.6,430.5,141.5,440,156.5,447.7z M40.6,272H8.5 c1.4,21.2,5.4,41.7,11.7,61.1L50,321.2C45.1,305.5,41.8,289,40.6,272z M40.6,240c1.4-18.8,5.2-37,11.1-54.1l-29.5-12.6 C14.7,194.3,10,216.7,8.5,240H40.6z M64.3,156.5c7.8-14.9,17.2-28.8,28.1-41.5L69.7,92.3c-13.7,15.6-25.5,32.8-34.9,51.5 L64.3,156.5z M397,419.6c-13.9,12-29.4,22.3-46.1,30.4l11.9,29.8c20.7-9.9,39.8-22.6,56.9-37.6L397,419.6z M115,92.4 c13.9-12,29.4-22.3,46.1-30.4l-11.9-29.8c-20.7,9.9-39.8,22.6-56.8,37.6L115,92.4z M447.7,355.5c-7.8,14.9-17.2,28.8-28.1,41.5 l22.7,22.7c13.7-15.6,25.5-32.9,34.9-51.5L447.7,355.5z M471.4,272c-1.4,18.8-5.2,37-11.1,54.1l29.5,12.6 c7.5-21.1,12.2-43.5,13.6-66.8H471.4z M321.2,462c-15.7,5-32.2,8.2-49.2,9.4v32.1c21.2-1.4,41.7-5.4,61.1-11.7L321.2,462z M240,471.4c-18.8-1.4-37-5.2-54.1-11.1l-12.6,29.5c21.1,7.5,43.5,12.2,66.8,13.6V471.4z M462,190.8c5,15.7,8.2,32.2,9.4,49.2h32.1 c-1.4-21.2-5.4-41.7-11.7-61.1L462,190.8z M92.4,397c-12-13.9-22.3-29.4-30.4-46.1l-29.8,11.9c9.9,20.7,22.6,39.8,37.6,56.9 L92.4,397z M272,40.6c18.8,1.4,36.9,5.2,54.1,11.1l12.6-29.5C317.7,14.7,295.3,10,272,8.5V40.6z M190.8,50 c15.7-5,32.2-8.2,49.2-9.4V8.5c-21.2,1.4-41.7,5.4-61.1,11.7L190.8,50z M442.3,92.3L419.6,115c12,13.9,22.3,29.4,30.5,46.1 l29.8-11.9C470,128.5,457.3,109.4,442.3,92.3z M397,92.4l22.7-22.7c-15.6-13.7-32.8-25.5-51.5-34.9l-12.6,29.5 C370.4,72.1,384.4,81.5,397,92.4z"})});var i=x(x({},o),{},{attributeName:"opacity"}),s={tag:"circle",attributes:x(x({},a),{},{cx:"256",cy:"364",r:"28"}),children:[]};return n||s.children.push({tag:"animate",attributes:x(x({},o),{},{attributeName:"r",values:"28;14;28;28;14;28;"})},{tag:"animate",attributes:x(x({},i),{},{values:"1;0;1;1;0;1;"})}),r.push(s),r.push({tag:"path",attributes:x(x({},a),{},{opacity:"1",d:"M263.7,312h-16c-6.6,0-12-5.4-12-12c0-71,77.4-63.9,77.4-107.8c0-20-17.8-40.2-57.4-40.2c-29.1,0-44.3,9.6-59.2,28.7 c-3.9,5-11.1,6-16.2,2.4l-13.1-9.2c-5.6-3.9-6.9-11.8-2.6-17.2c21.2-27.2,46.4-44.7,91.2-44.7c52.3,0,97.4,29.8,97.4,80.2 c0,67.6-77.4,63.5-77.4,107.8C275.7,306.6,270.3,312,263.7,312z"}),children:n?[]:[{tag:"animate",attributes:x(x({},i),{},{values:"1;0;0;0;0;1;"})}]}),n||r.push({tag:"path",attributes:x(x({},a),{},{opacity:"0",d:"M232.5,134.5l7,168c0.3,6.4,5.6,11.5,12,11.5h9c6.4,0,11.7-5.1,12-11.5l7-168c0.3-6.8-5.2-12.5-12-12.5h-23 C237.7,122,232.2,127.7,232.5,134.5z"}),children:[{tag:"animate",attributes:x(x({},i),{},{values:"0;0;1;1;0;0;"})}]}),{tag:"g",attributes:{class:"missing"},children:r}}}},n1={hooks:function(){return{parseNodeAttributes:function(n,r){var a=r.getAttribute("data-fa-symbol"),o=a===null?!1:a===""?!0:a;return n.symbol=o,n}}}},r1=[tg,Wg,Vg,Bg,$g,Jg,Kg,Xg,e1,t1,n1];gg(r1,{mixoutsTo:Ce});Ce.noAuto;Ce.config;Ce.library;Ce.dom;var hs=Ce.parse;Ce.findIconDefinition;Ce.toHtml;var a1=Ce.icon;Ce.layer;Ce.text;Ce.counter;var N={},o1={get exports(){return N},set exports(e){N=e}},i1="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED",s1=i1,l1=s1;function Hh(){}function Uh(){}Uh.resetWarningCache=Hh;var c1=function(){function e(r,a,o,i,s,l){if(l!==l1){var c=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw c.name="Invariant Violation",c}}e.isRequired=e;function t(){return e}var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:Uh,resetWarningCache:Hh};return n.PropTypes=n,n};o1.exports=c1();function ru(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),n.push.apply(n,r)}return n}function wt(e){for(var t=1;t=0)&&(n[a]=e[a]);return n}function d1(e,t){if(e==null)return{};var n=u1(e,t),r,a;if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function fs(e){return h1(e)||f1(e)||p1(e)||m1()}function h1(e){if(Array.isArray(e))return ps(e)}function f1(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function p1(e,t){if(e){if(typeof e=="string")return ps(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return ps(e,t)}}function ps(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&arguments[2]!==void 0?arguments[2]:{};if(typeof t=="string")return t;var r=(t.children||[]).map(function(l){return Vh(e,l)}),a=Object.keys(t.attributes||{}).reduce(function(l,c){var d=t.attributes[c];switch(c){case"class":l.attrs.className=d,delete t.attributes.class;break;case"style":l.attrs.style=b1(d);break;default:c.indexOf("aria-")===0||c.indexOf("data-")===0?l.attrs[c.toLowerCase()]=d:l.attrs[Wh(c)]=d}return l},{attrs:{}}),o=n.style,i=o===void 0?{}:o,s=d1(n,y1);return a.attrs.style=wt(wt({},a.attrs.style),i),e.apply(void 0,[t.tag,wt(wt({},a.attrs),s)].concat(fs(r)))}var Bh=!1;try{Bh=!0}catch{}function k1(){if(!Bh&&console&&typeof console.error=="function"){var e;(e=console).error.apply(e,arguments)}}function au(e){if(e&&ao(e)==="object"&&e.prefix&&e.iconName&&e.icon)return e;if(hs.icon)return hs.icon(e);if(e===null)return null;if(e&&ao(e)==="object"&&e.prefix&&e.iconName)return e;if(Array.isArray(e)&&e.length===2)return{prefix:e[0],iconName:e[1]};if(typeof e=="string")return{prefix:"fas",iconName:e}}function ii(e,t){return Array.isArray(t)&&t.length>0||!Array.isArray(t)&&t?yn({},e,t):{}}var Mn=io.forwardRef(function(e,t){var n=e.icon,r=e.mask,a=e.symbol,o=e.className,i=e.title,s=e.titleId,l=e.maskId,c=au(n),d=ii("classes",[].concat(fs(g1(e)),fs(o.split(" ")))),p=ii("transform",typeof e.transform=="string"?hs.transform(e.transform):e.transform),m=ii("mask",au(r)),g=a1(c,wt(wt(wt(wt({},d),p),m),{},{symbol:a,title:i,titleId:s,maskId:l}));if(!g)return k1("Could not find icon",c),null;var y=g.abstract,b={ref:t};return Object.keys(e).forEach(function(P){Mn.defaultProps.hasOwnProperty(P)||(b[P]=e[P])}),x1(y[0],b)});Mn.displayName="FontAwesomeIcon";Mn.propTypes={beat:N.bool,border:N.bool,beatFade:N.bool,bounce:N.bool,className:N.string,fade:N.bool,flash:N.bool,mask:N.oneOfType([N.object,N.array,N.string]),maskId:N.string,fixedWidth:N.bool,inverse:N.bool,flip:N.oneOf([!0,!1,"horizontal","vertical","both"]),icon:N.oneOfType([N.object,N.array,N.string]),listItem:N.bool,pull:N.oneOf(["right","left"]),pulse:N.bool,rotation:N.oneOf([0,90,180,270]),shake:N.bool,size:N.oneOf(["2xs","xs","sm","lg","xl","2xl","1x","2x","3x","4x","5x","6x","7x","8x","9x","10x"]),spin:N.bool,spinPulse:N.bool,spinReverse:N.bool,symbol:N.oneOfType([N.bool,N.string]),title:N.string,titleId:N.string,transform:N.oneOfType([N.string,N.object]),swapOpacity:N.bool};Mn.defaultProps={border:!1,className:"",mask:null,maskId:null,fixedWidth:!1,inverse:!1,flip:!1,icon:null,listItem:!1,pull:null,pulse:!1,rotation:null,size:null,spin:!1,spinPulse:!1,spinReverse:!1,beat:!1,fade:!1,beatFade:!1,bounce:!1,shake:!1,symbol:!1,title:"",titleId:null,transform:null,swapOpacity:!1};var x1=Vh.bind(null,io.createElement),S1={prefix:"fab",iconName:"css3-alt",icon:[384,512,[],"f38b","M0 32l34.9 395.8L192 480l157.1-52.2L384 32H0zm313.1 80l-4.8 47.3L193 208.6l-.3.1h111.5l-12.8 146.6-98.2 28.7-98.8-29.2-6.4-73.9h48.9l3.2 38.3 52.6 13.3 54.7-15.4 3.7-61.6-166.3-.5v-.1l-.2.1-3.6-46.3L193.1 162l6.5-2.7H76.7L70.9 112h242.2z"]},I1={prefix:"fab",iconName:"markdown",icon:[640,512,[],"f60f","M593.8 59.1H46.2C20.7 59.1 0 79.8 0 105.2v301.5c0 25.5 20.7 46.2 46.2 46.2h547.7c25.5 0 46.2-20.7 46.1-46.1V105.2c0-25.4-20.7-46.1-46.2-46.1zM338.5 360.6H277v-120l-61.5 76.9-61.5-76.9v120H92.3V151.4h61.5l61.5 76.9 61.5-76.9h61.5v209.2zm135.3 3.1L381.5 256H443V151.4h61.5V256H566z"]},T1={prefix:"fab",iconName:"node",icon:[640,512,[],"f419","M316.3 452c-2.1 0-4.2-.6-6.1-1.6L291 439c-2.9-1.6-1.5-2.2-.5-2.5 3.8-1.3 4.6-1.6 8.7-4 .4-.2 1-.1 1.4.1l14.8 8.8c.5.3 1.3.3 1.8 0L375 408c.5-.3.9-.9.9-1.6v-66.7c0-.7-.3-1.3-.9-1.6l-57.8-33.3c-.5-.3-1.2-.3-1.8 0l-57.8 33.3c-.6.3-.9 1-.9 1.6v66.7c0 .6.4 1.2.9 1.5l15.8 9.1c8.6 4.3 13.9-.8 13.9-5.8v-65.9c0-.9.7-1.7 1.7-1.7h7.3c.9 0 1.7.7 1.7 1.7v65.9c0 11.5-6.2 18-17.1 18-3.3 0-6 0-13.3-3.6l-15.2-8.7c-3.7-2.2-6.1-6.2-6.1-10.5v-66.7c0-4.3 2.3-8.4 6.1-10.5l57.8-33.4c3.7-2.1 8.5-2.1 12.1 0l57.8 33.4c3.7 2.2 6.1 6.2 6.1 10.5v66.7c0 4.3-2.3 8.4-6.1 10.5l-57.8 33.4c-1.7 1.1-3.8 1.7-6 1.7zm46.7-65.8c0-12.5-8.4-15.8-26.2-18.2-18-2.4-19.8-3.6-19.8-7.8 0-3.5 1.5-8.1 14.8-8.1 11.9 0 16.3 2.6 18.1 10.6.2.8.8 1.3 1.6 1.3h7.5c.5 0 .9-.2 1.2-.5.3-.4.5-.8.4-1.3-1.2-13.8-10.3-20.2-28.8-20.2-16.5 0-26.3 7-26.3 18.6 0 12.7 9.8 16.1 25.6 17.7 18.9 1.9 20.4 4.6 20.4 8.3 0 6.5-5.2 9.2-17.4 9.2-15.3 0-18.7-3.8-19.8-11.4-.1-.8-.8-1.4-1.7-1.4h-7.5c-.9 0-1.7.7-1.7 1.7 0 9.7 5.3 21.3 30.6 21.3 18.5 0 29-7.2 29-19.8zm54.5-50.1c0 6.1-5 11.1-11.1 11.1s-11.1-5-11.1-11.1c0-6.3 5.2-11.1 11.1-11.1 6-.1 11.1 4.8 11.1 11.1zm-1.8 0c0-5.2-4.2-9.3-9.4-9.3-5.1 0-9.3 4.1-9.3 9.3 0 5.2 4.2 9.4 9.3 9.4 5.2-.1 9.4-4.3 9.4-9.4zm-4.5 6.2h-2.6c-.1-.6-.5-3.8-.5-3.9-.2-.7-.4-1.1-1.3-1.1h-2.2v5h-2.4v-12.5h4.3c1.5 0 4.4 0 4.4 3.3 0 2.3-1.5 2.8-2.4 3.1 1.7.1 1.8 1.2 2.1 2.8.1 1 .3 2.7.6 3.3zm-2.8-8.8c0-1.7-1.2-1.7-1.8-1.7h-2v3.5h1.9c1.6 0 1.9-1.1 1.9-1.8zM137.3 191c0-2.7-1.4-5.1-3.7-6.4l-61.3-35.3c-1-.6-2.2-.9-3.4-1h-.6c-1.2 0-2.3.4-3.4 1L3.7 184.6C1.4 185.9 0 188.4 0 191l.1 95c0 1.3.7 2.5 1.8 3.2 1.1.7 2.5.7 3.7 0L42 268.3c2.3-1.4 3.7-3.8 3.7-6.4v-44.4c0-2.6 1.4-5.1 3.7-6.4l15.5-8.9c1.2-.7 2.4-1 3.7-1 1.3 0 2.6.3 3.7 1l15.5 8.9c2.3 1.3 3.7 3.8 3.7 6.4v44.4c0 2.6 1.4 5.1 3.7 6.4l36.4 20.9c1.1.7 2.6.7 3.7 0 1.1-.6 1.8-1.9 1.8-3.2l.2-95zM472.5 87.3v176.4c0 2.6-1.4 5.1-3.7 6.4l-61.3 35.4c-2.3 1.3-5.1 1.3-7.4 0l-61.3-35.4c-2.3-1.3-3.7-3.8-3.7-6.4v-70.8c0-2.6 1.4-5.1 3.7-6.4l61.3-35.4c2.3-1.3 5.1-1.3 7.4 0l15.3 8.8c1.7 1 3.9-.3 3.9-2.2v-94c0-2.8 3-4.6 5.5-3.2l36.5 20.4c2.3 1.2 3.8 3.7 3.8 6.4zm-46 128.9c0-.7-.4-1.3-.9-1.6l-21-12.2c-.6-.3-1.3-.3-1.9 0l-21 12.2c-.6.3-.9.9-.9 1.6v24.3c0 .7.4 1.3.9 1.6l21 12.1c.6.3 1.3.3 1.8 0l21-12.1c.6-.3.9-.9.9-1.6v-24.3zm209.8-.7c2.3-1.3 3.7-3.8 3.7-6.4V192c0-2.6-1.4-5.1-3.7-6.4l-60.9-35.4c-2.3-1.3-5.1-1.3-7.4 0l-61.3 35.4c-2.3 1.3-3.7 3.8-3.7 6.4v70.8c0 2.7 1.4 5.1 3.7 6.4l60.9 34.7c2.2 1.3 5 1.3 7.3 0l36.8-20.5c2.5-1.4 2.5-5 0-6.4L550 241.6c-1.2-.7-1.9-1.9-1.9-3.2v-22.2c0-1.3.7-2.5 1.9-3.2l19.2-11.1c1.1-.7 2.6-.7 3.7 0l19.2 11.1c1.1.7 1.9 1.9 1.9 3.2v17.4c0 2.8 3.1 4.6 5.6 3.2l36.7-21.3zM559 219c-.4.3-.7.7-.7 1.2v13.6c0 .5.3 1 .7 1.2l11.8 6.8c.4.3 1 .3 1.4 0L584 235c.4-.3.7-.7.7-1.2v-13.6c0-.5-.3-1-.7-1.2l-11.8-6.8c-.4-.3-1-.3-1.4 0L559 219zm-254.2 43.5v-70.4c0-2.6-1.6-5.1-3.9-6.4l-61.1-35.2c-2.1-1.2-5-1.4-7.4 0l-61.1 35.2c-2.3 1.3-3.9 3.7-3.9 6.4v70.4c0 2.8 1.9 5.2 4 6.4l61.2 35.2c2.4 1.4 5.2 1.3 7.4 0l61-35.2c1.8-1 3.1-2.7 3.6-4.7.1-.5.2-1.1.2-1.7zm-74.3-124.9l-.8.5h1.1l-.3-.5zm76.2 130.2l-.4-.7v.9l.4-.2z"]},_1={prefix:"fab",iconName:"yarn",icon:[496,512,[],"f7e3","M393.9 345.2c-39 9.3-48.4 32.1-104 47.4 0 0-2.7 4-10.4 5.8-13.4 3.3-63.9 6-68.5 6.1-12.4.1-19.9-3.2-22-8.2-6.4-15.3 9.2-22 9.2-22-8.1-5-9-9.9-9.8-8.1-2.4 5.8-3.6 20.1-10.1 26.5-8.8 8.9-25.5 5.9-35.3.8-10.8-5.7.8-19.2.8-19.2s-5.8 3.4-10.5-3.6c-6-9.3-17.1-37.3 11.5-62-1.3-10.1-4.6-53.7 40.6-85.6 0 0-20.6-22.8-12.9-43.3 5-13.4 7-13.3 8.6-13.9 5.7-2.2 11.3-4.6 15.4-9.1 20.6-22.2 46.8-18 46.8-18s12.4-37.8 23.9-30.4c3.5 2.3 16.3 30.6 16.3 30.6s13.6-7.9 15.1-5c8.2 16 9.2 46.5 5.6 65.1-6.1 30.6-21.4 47.1-27.6 57.5-1.4 2.4 16.5 10 27.8 41.3 10.4 28.6 1.1 52.7 2.8 55.3.8 1.4 13.7.8 36.4-13.2 12.8-7.9 28.1-16.9 45.4-17 16.7-.5 17.6 19.2 4.9 22.2zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-79.3 75.2c-1.7-13.6-13.2-23-28-22.8-22 .3-40.5 11.7-52.8 19.2-4.8 3-8.9 5.2-12.4 6.8 3.1-44.5-22.5-73.1-28.7-79.4 7.8-11.3 18.4-27.8 23.4-53.2 4.3-21.7 3-55.5-6.9-74.5-1.6-3.1-7.4-11.2-21-7.4-9.7-20-13-22.1-15.6-23.8-1.1-.7-23.6-16.4-41.4 28-12.2.9-31.3 5.3-47.5 22.8-2 2.2-5.9 3.8-10.1 5.4h.1c-8.4 3-12.3 9.9-16.9 22.3-6.5 17.4.2 34.6 6.8 45.7-17.8 15.9-37 39.8-35.7 82.5-34 36-11.8 73-5.6 79.6-1.6 11.1 3.7 19.4 12 23.8 12.6 6.7 30.3 9.6 43.9 2.8 4.9 5.2 13.8 10.1 30 10.1 6.8 0 58-2.9 72.6-6.5 6.8-1.6 11.5-4.5 14.6-7.1 9.8-3.1 36.8-12.3 62.2-28.7 18-11.7 24.2-14.2 37.6-17.4 12.9-3.2 21-15.1 19.4-28.2z"]},E1={prefix:"fab",iconName:"react",icon:[512,512,[],"f41b","M418.2 177.2c-5.4-1.8-10.8-3.5-16.2-5.1.9-3.7 1.7-7.4 2.5-11.1 12.3-59.6 4.2-107.5-23.1-123.3-26.3-15.1-69.2.6-112.6 38.4-4.3 3.7-8.5 7.6-12.5 11.5-2.7-2.6-5.5-5.2-8.3-7.7-45.5-40.4-91.1-57.4-118.4-41.5-26.2 15.2-34 60.3-23 116.7 1.1 5.6 2.3 11.1 3.7 16.7-6.4 1.8-12.7 3.8-18.6 5.9C38.3 196.2 0 225.4 0 255.6c0 31.2 40.8 62.5 96.3 81.5 4.5 1.5 9 3 13.6 4.3-1.5 6-2.8 11.9-4 18-10.5 55.5-2.3 99.5 23.9 114.6 27 15.6 72.4-.4 116.6-39.1 3.5-3.1 7-6.3 10.5-9.7 4.4 4.3 9 8.4 13.6 12.4 42.8 36.8 85.1 51.7 111.2 36.6 27-15.6 35.8-62.9 24.4-120.5-.9-4.4-1.9-8.9-3-13.5 3.2-.9 6.3-1.9 9.4-2.9 57.7-19.1 99.5-50 99.5-81.7 0-30.3-39.4-59.7-93.8-78.4zM282.9 92.3c37.2-32.4 71.9-45.1 87.7-36 16.9 9.7 23.4 48.9 12.8 100.4-.7 3.4-1.4 6.7-2.3 10-22.2-5-44.7-8.6-67.3-10.6-13-18.6-27.2-36.4-42.6-53.1 3.9-3.7 7.7-7.2 11.7-10.7zM167.2 307.5c5.1 8.7 10.3 17.4 15.8 25.9-15.6-1.7-31.1-4.2-46.4-7.5 4.4-14.4 9.9-29.3 16.3-44.5 4.6 8.8 9.3 17.5 14.3 26.1zm-30.3-120.3c14.4-3.2 29.7-5.8 45.6-7.8-5.3 8.3-10.5 16.8-15.4 25.4-4.9 8.5-9.7 17.2-14.2 26-6.3-14.9-11.6-29.5-16-43.6zm27.4 68.9c6.6-13.8 13.8-27.3 21.4-40.6s15.8-26.2 24.4-38.9c15-1.1 30.3-1.7 45.9-1.7s31 .6 45.9 1.7c8.5 12.6 16.6 25.5 24.3 38.7s14.9 26.7 21.7 40.4c-6.7 13.8-13.9 27.4-21.6 40.8-7.6 13.3-15.7 26.2-24.2 39-14.9 1.1-30.4 1.6-46.1 1.6s-30.9-.5-45.6-1.4c-8.7-12.7-16.9-25.7-24.6-39s-14.8-26.8-21.5-40.6zm180.6 51.2c5.1-8.8 9.9-17.7 14.6-26.7 6.4 14.5 12 29.2 16.9 44.3-15.5 3.5-31.2 6.2-47 8 5.4-8.4 10.5-17 15.5-25.6zm14.4-76.5c-4.7-8.8-9.5-17.6-14.5-26.2-4.9-8.5-10-16.9-15.3-25.2 16.1 2 31.5 4.7 45.9 8-4.6 14.8-10 29.2-16.1 43.4zM256.2 118.3c10.5 11.4 20.4 23.4 29.6 35.8-19.8-.9-39.7-.9-59.5 0 9.8-12.9 19.9-24.9 29.9-35.8zM140.2 57c16.8-9.8 54.1 4.2 93.4 39 2.5 2.2 5 4.6 7.6 7-15.5 16.7-29.8 34.5-42.9 53.1-22.6 2-45 5.5-67.2 10.4-1.3-5.1-2.4-10.3-3.5-15.5-9.4-48.4-3.2-84.9 12.6-94zm-24.5 263.6c-4.2-1.2-8.3-2.5-12.4-3.9-21.3-6.7-45.5-17.3-63-31.2-10.1-7-16.9-17.8-18.8-29.9 0-18.3 31.6-41.7 77.2-57.6 5.7-2 11.5-3.8 17.3-5.5 6.8 21.7 15 43 24.5 63.6-9.6 20.9-17.9 42.5-24.8 64.5zm116.6 98c-16.5 15.1-35.6 27.1-56.4 35.3-11.1 5.3-23.9 5.8-35.3 1.3-15.9-9.2-22.5-44.5-13.5-92 1.1-5.6 2.3-11.2 3.7-16.7 22.4 4.8 45 8.1 67.9 9.8 13.2 18.7 27.7 36.6 43.2 53.4-3.2 3.1-6.4 6.1-9.6 8.9zm24.5-24.3c-10.2-11-20.4-23.2-30.3-36.3 9.6.4 19.5.6 29.5.6 10.3 0 20.4-.2 30.4-.7-9.2 12.7-19.1 24.8-29.6 36.4zm130.7 30c-.9 12.2-6.9 23.6-16.5 31.3-15.9 9.2-49.8-2.8-86.4-34.2-4.2-3.6-8.4-7.5-12.7-11.5 15.3-16.9 29.4-34.8 42.2-53.6 22.9-1.9 45.7-5.4 68.2-10.5 1 4.1 1.9 8.2 2.7 12.2 4.9 21.6 5.7 44.1 2.5 66.3zm18.2-107.5c-2.8.9-5.6 1.8-8.5 2.6-7-21.8-15.6-43.1-25.5-63.8 9.6-20.4 17.7-41.4 24.5-62.9 5.2 1.5 10.2 3.1 15 4.7 46.6 16 79.3 39.8 79.3 58 0 19.6-34.9 44.9-84.8 61.4zm-149.7-15c25.3 0 45.8-20.5 45.8-45.8s-20.5-45.8-45.8-45.8c-25.3 0-45.8 20.5-45.8 45.8s20.5 45.8 45.8 45.8z"]},A1={prefix:"fab",iconName:"linkedin",icon:[448,512,[],"f08c","M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"]},C1={prefix:"fab",iconName:"jira",icon:[496,512,[],"f7b1","M490 241.7C417.1 169 320.6 71.8 248.5 0 83 164.9 6 241.7 6 241.7c-7.9 7.9-7.9 20.7 0 28.7C138.8 402.7 67.8 331.9 248.5 512c379.4-378 15.7-16.7 241.5-241.7 8-7.9 8-20.7 0-28.6zm-241.5 90l-76-75.7 76-75.7 76 75.7-76 75.7z"]},P1={prefix:"fab",iconName:"html5",icon:[384,512,[],"f13b","M0 32l34.9 395.8L191.5 480l157.6-52.2L384 32H0zm308.2 127.9H124.4l4.1 49.4h175.6l-13.6 148.4-97.9 27v.3h-1.1l-98.7-27.3-6-75.8h47.7L138 320l53.5 14.5 53.7-14.5 6-62.2H84.3L71.5 112.2h241.1l-4.4 47.7z"]},j1={prefix:"fab",iconName:"trello",icon:[448,512,[],"f181","M392.3 32H56.1C25.1 32 0 57.1 0 88c-.1 0 0-4 0 336 0 30.9 25.1 56 56 56h336.2c30.8-.2 55.7-25.2 55.7-56V88c.1-30.8-24.8-55.8-55.6-56zM197 371.3c-.2 14.7-12.1 26.6-26.9 26.6H87.4c-14.8.1-26.9-11.8-27-26.6V117.1c0-14.8 12-26.9 26.9-26.9h82.9c14.8 0 26.9 12 26.9 26.9v254.2zm193.1-112c0 14.8-12 26.9-26.9 26.9h-81c-14.8 0-26.9-12-26.9-26.9V117.2c0-14.8 12-26.9 26.8-26.9h81.1c14.8 0 26.9 12 26.9 26.9v142.1z"]},N1={prefix:"fab",iconName:"js",icon:[448,512,[],"f3b8","M0 32v448h448V32H0zm243.8 349.4c0 43.6-25.6 63.5-62.9 63.5-33.7 0-53.2-17.4-63.2-38.5l34.3-20.7c6.6 11.7 12.6 21.6 27.1 21.6 13.8 0 22.6-5.4 22.6-26.5V237.7h42.1v143.7zm99.6 63.5c-39.1 0-64.4-18.6-76.7-43l34.3-19.8c9 14.7 20.8 25.6 41.5 25.6 17.4 0 28.6-8.7 28.6-20.8 0-14.4-11.4-19.5-30.7-28l-10.5-4.5c-30.4-12.9-50.5-29.2-50.5-63.5 0-31.6 24.1-55.6 61.6-55.6 26.8 0 46 9.3 59.8 33.7L368 290c-7.2-12.9-15-18-27.1-18-12.3 0-20.1 7.8-20.1 18 0 12.6 7.8 17.7 25.9 25.6l10.5 4.5c35.8 15.3 55.9 31 55.9 66.2 0 37.8-29.8 58.6-69.7 58.6z"]},z1={prefix:"fab",iconName:"git",icon:[512,512,[],"f1d3","M216.29 158.39H137C97 147.9 6.51 150.63 6.51 233.18c0 30.09 15 51.23 35 61-25.1 23-37 33.85-37 49.21 0 11 4.47 21.14 17.89 26.81C8.13 383.61 0 393.35 0 411.65c0 32.11 28.05 50.82 101.63 50.82 70.75 0 111.79-26.42 111.79-73.18 0-58.66-45.16-56.5-151.63-63l13.43-21.55c27.27 7.58 118.7 10 118.7-67.89 0-18.7-7.73-31.71-15-41.07l37.41-2.84zm-63.42 241.9c0 32.06-104.89 32.1-104.89 2.43 0-8.14 5.27-15 10.57-21.54 77.71 5.3 94.32 3.37 94.32 19.11zm-50.81-134.58c-52.8 0-50.46-71.16 1.2-71.16 49.54 0 50.82 71.16-1.2 71.16zm133.3 100.51v-32.1c26.75-3.66 27.24-2 27.24-11V203.61c0-8.5-2.05-7.38-27.24-16.26l4.47-32.92H324v168.71c0 6.51.4 7.32 6.51 8.14l20.73 2.84v32.1zm52.45-244.31c-23.17 0-36.59-13.43-36.59-36.61s13.42-35.77 36.59-35.77c23.58 0 37 12.62 37 35.77s-13.42 36.61-37 36.61zM512 350.46c-17.49 8.53-43.1 16.26-66.28 16.26-48.38 0-66.67-19.5-66.67-65.46V194.75c0-5.42 1.05-4.06-31.71-4.06V154.5c35.78-4.07 50-22 54.47-66.27h38.63c0 65.83-1.34 61.81 3.26 61.81H501v40.65h-60.56v97.15c0 6.92-4.92 51.41 60.57 26.84z"]},O1={prefix:"fab",iconName:"sketch",icon:[512,512,[],"f7c6","M27.5 162.2L9 187.1h90.5l6.9-130.7-78.9 105.8zM396.3 45.7L267.7 32l135.7 147.2-7.1-133.5zM112.2 218.3l-11.2-22H9.9L234.8 458zm2-31.2h284l-81.5-88.5L256.3 33zm297.3 9.1L277.6 458l224.8-261.7h-90.9zM415.4 69L406 56.4l.9 17.3 6.1 113.4h90.3zM113.5 93.5l-4.6 85.6L244.7 32 116.1 45.7zm287.7 102.7h-290l42.4 82.9L256.3 480l144.9-283.8z"]},$h={prefix:"fab",iconName:"github",icon:[496,512,[],"f09b","M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"]},R1={prefix:"fab",iconName:"npm",icon:[576,512,[],"f3d4","M288 288h-32v-64h32v64zm288-128v192H288v32H160v-32H0V160h576zm-416 32H32v128h64v-96h32v96h32V192zm160 0H192v160h64v-32h64V192zm224 0H352v128h64v-96h32v96h32v-96h32v96h32V192z"]},F1={prefix:"fab",iconName:"medium",icon:[640,512,[62407,"medium-m"],"f23a","M180.5,74.262C80.813,74.262,0,155.633,0,256S80.819,437.738,180.5,437.738,361,356.373,361,256,280.191,74.262,180.5,74.262Zm288.25,10.646c-49.845,0-90.245,76.619-90.245,171.095s40.406,171.1,90.251,171.1,90.251-76.619,90.251-171.1H559C559,161.5,518.6,84.908,468.752,84.908Zm139.506,17.821c-17.526,0-31.735,68.628-31.735,153.274s14.2,153.274,31.735,153.274S640,340.631,640,256C640,171.351,625.785,102.729,608.258,102.729Z"]},L1={prefix:"fas",iconName:"mug-hot",icon:[512,512,[9749],"f7b6","M88 0C74.7 0 64 10.7 64 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C120.5 112.3 128 119.9 128 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C119.5 47.7 112 40.1 112 24c0-13.3-10.7-24-24-24zM32 192c-17.7 0-32 14.3-32 32V416c0 53 43 96 96 96H288c53 0 96-43 96-96h16c61.9 0 112-50.1 112-112s-50.1-112-112-112H352 32zm352 64h16c26.5 0 48 21.5 48 48s-21.5 48-48 48H384V256zM224 24c0-13.3-10.7-24-24-24s-24 10.7-24 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C232.5 112.3 240 119.9 240 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C231.5 47.7 224 40.1 224 24z"]},M1={prefix:"fas",iconName:"envelope",icon:[512,512,[128386,9993,61443],"f0e0","M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"]},D1={prefix:"fas",iconName:"keyboard",icon:[576,512,[9e3],"f11c","M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zm16 64h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM64 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V240zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V336c0-8.8 7.2-16 16-16zm80-176c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V144zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zM160 336c0-8.8 7.2-16 16-16H400c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V336zM272 128h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H272c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM256 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H272c-8.8 0-16-7.2-16-16V240zM368 128h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM352 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V240zM464 128h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM448 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V240zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V336c0-8.8 7.2-16 16-16z"]},H1={prefix:"fas",iconName:"file",icon:[384,512,[128196,128459,61462],"f15b","M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0L384 128z"]};const U1=[{name:"React",logo:E1,link:"https://react.dev/"},{name:"Yarn",logo:_1,link:"https://classic.yarnpkg.com/lang/en/"},{name:"Javascript",logo:N1,link:"https://www.javascript.com/"},{name:"CSS",logo:S1,link:"https://developer.mozilla.org/en-US/docs/Web/CSS"},{name:"Git",logo:z1,link:"https://git-scm.com/"},{name:"JIRA",logo:C1,link:"https://www.atlassian.com/software/jira"},{name:"Node",logo:T1,link:"https://nodejs.org/en"},{name:"Github",logo:$h,link:"https://github.com/"},{name:"HTML5",logo:P1,link:"https://developer.mozilla.org/en-US/docs/Glossary/HTML5"},{name:"Trello",logo:j1,link:"https://www.atlassian.com/software/trello"},{name:"TypeScript",logo:D1,link:"https://www.typescriptlang.org/"},{name:"Sketch",logo:O1,link:"https://www.sketch.com/"},{name:"Mocha",logo:L1,link:"https://mochajs.org/"},{name:"npm",logo:R1,link:"https://npmjs.com/"},{name:"Markdown",logo:I1,link:"https://www.markdownguide.org/"}];function W1(){return ie("div",{children:[C("h2",{id:"tech-stack-h2",children:"Tech Stack"}),C("div",{id:"tech-stack",children:U1.map(e=>C("div",{className:"techIconDiv",children:ie("a",{href:e.link,target:"_blank",children:[C(Mn,{className:"techIcon",icon:e.logo}),C("p",{className:"hide",children:e.name})]})},e.name))})]})}const V1="Stories by Spencer Attick on Medium",B1="Stories by Spencer Attick on Medium",$1="https://medium.com/@spencer.attick?source=rss-e5dc359f27c2------2",G1="https://cdn-images-1.medium.com/fit/c/150/150/1*_8WJXYOSojpzAxZ8hddk4g.jpeg",Y1=[],q1=[{id:"https://medium.com/p/0793f2ee2f7e",title:"Project: Learning Python by Coding Simple TicTacToe Game",link:"https://medium.com/@spencer.attick/project-learning-python-by-coding-simple-tictactoe-game-0793f2ee2f7e?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1710348622e3,created:1710348622e3,category:["software-development","study","coding","javascript","python"],content:'

As a trained JavaScript developer working for years in the language, I’d been wanting to branch out into the functional possibilities of other languages. I took Codecademy’s Computer Science path which is taught in Python and was eager to step into a new challenge. As a part of my learning, I built a simple command line Tic Tac Toe game to solidify some Python syntax and concepts.

The Problem

To this point, I’ve gained extensive knowledge and time spent working with JavaScript and was ready to up my skill game by learning Python.

The Work

Codecademy’s Computer Science track offers a great intro to Python at the start of the course. I learned through their project sets to come to a working knowledge of the Python language.

The code base for this project consists of just two files: one containing a Board class that holds the methods and variables needed to run the game and the other is a brief file for executing the logic of the program.

The Code

You can find the code here: https://github.com/spencerattick/tic_tac_toe.

What I Learned

This project was a great experience! I had actually worked on something similar in JavaScript but through a bit of up front planning, managed to code this Python version much more succinctly. This work gave me a great opporunity to code outside of Codecademy’s guardrails to stand something up on my own. It was a wonderful way to solidify and further practice the new Python syntax I had learned.

',enclosures:[],content_encoded:'

As a trained JavaScript developer working for years in the language, I’d been wanting to branch out into the functional possibilities of other languages. I took Codecademy’s Computer Science path which is taught in Python and was eager to step into a new challenge. As a part of my learning, I built a simple command line Tic Tac Toe game to solidify some Python syntax and concepts.

The Problem

To this point, I’ve gained extensive knowledge and time spent working with JavaScript and was ready to up my skill game by learning Python.

The Work

Codecademy’s Computer Science track offers a great intro to Python at the start of the course. I learned through their project sets to come to a working knowledge of the Python language.

The code base for this project consists of just two files: one containing a Board class that holds the methods and variables needed to run the game and the other is a brief file for executing the logic of the program.

The Code

You can find the code here: https://github.com/spencerattick/tic_tac_toe.

What I Learned

This project was a great experience! I had actually worked on something similar in JavaScript but through a bit of up front planning, managed to code this Python version much more succinctly. This work gave me a great opporunity to code outside of Codecademy’s guardrails to stand something up on my own. It was a wonderful way to solidify and further practice the new Python syntax I had learned.

',media:{}},{id:"https://medium.com/p/40244b17df2c",title:"Integrating with the Strava API",link:"https://levelup.gitconnected.com/integrating-with-the-strava-api-40244b17df2c?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1682603854e3,created:1682603854e3,category:["javascript","code","strava","technology","api"],content:`
Alessio Soggetti for Unsplash

As I was coding my personal website I was looking for ways that I could personalize the project. I didn’t want it to just be a place to house blog posts and project walkthroughs. I wanted to share a bit of myself there as well.

With that in mind, I integrated the Goodreads RSS feed as books are one of my passions. While I was working on my portfolio I was also training for my first half-marathon and doing a TON of running that I was recording on Strava. I thought to myself, why not add that data onto my website as well?

I saw that they have an API that uses OAuth2, with which I didn’t have much experience. After a bit of trial and error along with a variety of internet resources I was able to get things up and running.

To save you some time, I thought I’d write a simple guide to the process. Strava does provide information here but I’ll pare things down for you even more step by step. Here is Strava’s documentation if you’d like the full rundown.

Let’s get started!

1. Get App Credentials from Strava
The first step you’ll need to take is to head over to Strava to register an app (it’s way easier than it sounds) and then get your access keys. The URL to do this can be hard to find so here it is for your reference: https://www.strava.com/settings/api.

Once you’re there, you’ll need to fill out a few fields in order to be provided with your credentials. The fields filled out here are not incredibly important.

Image from Strava’s documentation

The field that tripped me up here was the Authorization Callback Domain. This is only used once for the next step so you can set it to localhost to get things working. You don’t need to have a localhost running or take any additional steps to get the Authorization Callback Domain to work, just fill in that form with localhostas the Authorization Callback Domain and that’s it.

2. Authorize Credentials in the Browser
This step only happens once and it involves visiting a specific URL in the browser so that you can authorize the use of the credentials Strava gave you in the previous step.

My goal in using the API was to use the List Athlete Activities endpoint which requires read_all access. You’ll need to do this step for any level of access you’d like, but the following URL will be for read_all access specifically.

What you’ll want to do here is paste this URL into your browser: // http://localhost/exchange_token?state=&code=1c49f418910a609b0097ae5ce0016c9b8141e8cd&scope=read,activity:read_all. Go ahead and provide the URL with the client_id from the your Strava account.

3. Get Strava Access Code
Awesome! When you visit the above URL you’ll be prompted to on the website to authorize your project with Strava:

Once you click Authorize, you’ll be redirected to a new URL that looks like this:http://localhost/exchange_token?state=&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&scope=read,activity:read_all.

You won’t see anything render on that page and that’s just fine. All you need there is the new URL.

I’ve redacted some the code value I received there as a query parameter but that is the field you’ll want to hold on to for the next step.

4. Get Strava Access and Refresh Tokens
After you’ve collected the code value, you’ll want to make an API request (you can use Postman, cURL, or any other API request tool or server to do this) to get the actual access token information.

The request will be a POST to this endpoint:

https://www.strava.com/oauth/token

You’ll want to provide the following as query parameters:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to authorization_code

The complete URL will look like this (I’ve redacted my own values):

https://www.strava.com/oauth/token?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&grant_type=authorization_code

If you’re using Postman, here is what the interface should look like:

Making that request will result in a response that looks like this:

{
"token_type": "Bearer",
"expires_at": 1681350948,
"expires_in": 21600,
"refresh_token": "25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"access_token": "87b0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"athlete": {
"id": xxxxxx,
"username": "xxxxxxx",
"resource_state": 2,
"firstname": "Spencer",
"lastname": "Attick",
"bio": null,
"city": "Oakland",
"state": "California",
"country": "United States",
"sex": null,
"premium": false,
"summit": false,
"created_at": "2015-04-01T00:46:54Z",
"updated_at": "2023-04-11T18:24:23Z",
"badge_type_id": 0,
"weight": 0.0,
"profile_medium": "https://graph.facebook.com/1226490129/picture?height=256&width=256",
"profile": "https://graph.facebook.com/1226490129/picture?height=256&width=256",
"friend": null,
"follower": null
}
}

The important fields to pay attention to here are expires_at, refresh_token, and access_token.

Strava’s access_token will expire at the expires_at time which is a Unix Epoch timestamp. We’ll talk about refreshing later, but for now let’s get some Strava data.

5. Get Workout Data from Strava
Alright! We can finally make a request to get the data we want from Strava!

Here you’ll want to make a GET request that looks like this:

curl --location 'https://www.strava.com/api/v3/athlete/activities' \\
--header 'Authorization: Bearer '

You’ll use the access_token you received above as the Bearer Token here. Ensure that you don’t have a request body at all for this step as that will throw an error.

Check out the response to that request. You’ve got Strava data you can work with! You can add this to your personal website, create a workout dashboard for yourself, create a leaderboard with friends, or whatever else you can think of!

7. Manage Refreshing the Access Token
The last thing we want to take care of is making sure you can refresh your token when it expires. To do this, you’ll want to keep track of the last expires_at field you received. When the current time is greater than the value of the expires_at value, it’s time for a new access_token.

You’ll make this POST request to get a new token:

curl --location --request POST 'https://www.strava.com/oauth/token

Here are the query params you’ll use and their values:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to refresh_token
refresh_token: the refresh_token from your last successful authentication request

Here is the full request with the query params in place:

?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&grant_type=refresh_token&refresh_token=25bd1cf38exxxxxxxxxxxxxxxxxxxxx'

The response will look like this:

{
"token_type": "Bearer",
"access_token": "6e6e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_at": 1681430228,
"expires_in": 21600,
"refresh_token": "25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Now you’re all set! You can make requests to Strava to get meaningful data on your activities and you can generate a new access token!

From here, you’ll want to put everything together into something you can use in your server. Here is the code I use for Strava integration that I wrote for my personal website:

const isStravaTokenExpired = (currentExpirationTime) => {
const currentEpochTime = Date.now();
if (currentExpirationTime === 'undefined') {
return \`There is an error with the currentEpirationTime, it's value is: \${currentExpirationTime}.\`
}
return currentEpochTime > currentExpirationTime;
}

const generateNewToken = async () => {
console.log('Generating new token...');
const requestOptions = {
method: 'POST',
redirect: 'follow'
};
const requestURL = \`https://www.strava.com/oauth/token?client_id=\${process.env.STRAVA_CLIENT_ID}&client_secret=\${process.env.STRAVA_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=\${process.env.STRAVA_CACHED_REFRESH_TOKEN}\`;

try {
let response = await fetch(requestURL, requestOptions);
response = await response.json();
if (response.message === 'Bad Request') {
console.log(response);
return;
}
return {
refreshToken: await response.refresh_token,
expirationTime: await response.expires_at,
accessToken: await response.access_token
}
} catch (error) {
console.log(error);
}

}



const persistNewTokenData = async (newTokenData) => {
// Read the .env file
const envBuffer = fs.readFileSync('.env');
const envConfig = dotenv.parse(envBuffer);

// Update the relevant key with the new value
envConfig['STRAVA_EXPIRATION_TIME'] = newTokenData.expirationTime,
envConfig['STRAVA_CACHED_REFRESH_TOKEN'] = newTokenData.refreshToken,
envConfig['STRAVA_CACHED_TOKEN'] = newTokenData.accessToken

// Write the updated key-value pair to the file
const envText = Object.keys(envConfig).map(key => \`\${key}=\${envConfig[key]}\`).join('\\n');
await fs.promises.writeFile('.env', envText);
};



const getStravaActivityData = async () => {
console.log('Requesting activity data from Strava...');
let myHeaders = new Headers();
myHeaders.append("Authorization", \`Bearer \${process.env.STRAVA_CACHED_TOKEN}\`);

const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};

try {
const response = await fetch("https://www.strava.com/api/v3/athlete/activities", requestOptions);
return response.json();

} catch (error) {
console.log('error', error)
}
}

//check to see if the expiration time has passed
const executeStravaLogic = async () => {
const isTokenExpired = isStravaTokenExpired(process.env.STRAVA_EXPIRATION_TIME);

if (typeof isTokenExpired === 'string') {
console.log('Please resolve the error with the current expiration time stored in the .env file.');
return;
} else if (isTokenExpired) {
console.log('The expiration time has passed. Generating a new token...');
//if yes - generate a new token
const newTokenData = await generateNewToken();
if (!newTokenData.expirationTime) {
console.log('There was an error getting refresh token data.')
return;
}
//save the new token info to .env
persistNewTokenData(newTokenData);

}
//make request to Strava activities endpoint
const stravaActivityData = await getStravaActivityData();
return stravaActivityData;
};

app.get('/api/strava', executeStravaLogic);

Feel free to use what works for you and scrap what doesn’t.

Enjoy!

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


Integrating with the Strava API was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

`,enclosures:[],content_encoded:`
Alessio Soggetti for Unsplash

As I was coding my personal website I was looking for ways that I could personalize the project. I didn’t want it to just be a place to house blog posts and project walkthroughs. I wanted to share a bit of myself there as well.

With that in mind, I integrated the Goodreads RSS feed as books are one of my passions. While I was working on my portfolio I was also training for my first half-marathon and doing a TON of running that I was recording on Strava. I thought to myself, why not add that data onto my website as well?

I saw that they have an API that uses OAuth2, with which I didn’t have much experience. After a bit of trial and error along with a variety of internet resources I was able to get things up and running.

To save you some time, I thought I’d write a simple guide to the process. Strava does provide information here but I’ll pare things down for you even more step by step. Here is Strava’s documentation if you’d like the full rundown.

Let’s get started!

1. Get App Credentials from Strava
The first step you’ll need to take is to head over to Strava to register an app (it’s way easier than it sounds) and then get your access keys. The URL to do this can be hard to find so here it is for your reference: https://www.strava.com/settings/api.

Once you’re there, you’ll need to fill out a few fields in order to be provided with your credentials. The fields filled out here are not incredibly important.

Image from Strava’s documentation

The field that tripped me up here was the Authorization Callback Domain. This is only used once for the next step so you can set it to localhost to get things working. You don’t need to have a localhost running or take any additional steps to get the Authorization Callback Domain to work, just fill in that form with localhostas the Authorization Callback Domain and that’s it.

2. Authorize Credentials in the Browser
This step only happens once and it involves visiting a specific URL in the browser so that you can authorize the use of the credentials Strava gave you in the previous step.

My goal in using the API was to use the List Athlete Activities endpoint which requires read_all access. You’ll need to do this step for any level of access you’d like, but the following URL will be for read_all access specifically.

What you’ll want to do here is paste this URL into your browser: // http://localhost/exchange_token?state=&code=1c49f418910a609b0097ae5ce0016c9b8141e8cd&scope=read,activity:read_all. Go ahead and provide the URL with the client_id from the your Strava account.

3. Get Strava Access Code
Awesome! When you visit the above URL you’ll be prompted to on the website to authorize your project with Strava:

Once you click Authorize, you’ll be redirected to a new URL that looks like this:http://localhost/exchange_token?state=&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&scope=read,activity:read_all.

You won’t see anything render on that page and that’s just fine. All you need there is the new URL.

I’ve redacted some the code value I received there as a query parameter but that is the field you’ll want to hold on to for the next step.

4. Get Strava Access and Refresh Tokens
After you’ve collected the code value, you’ll want to make an API request (you can use Postman, cURL, or any other API request tool or server to do this) to get the actual access token information.

The request will be a POST to this endpoint:

https://www.strava.com/oauth/token

You’ll want to provide the following as query parameters:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to authorization_code

The complete URL will look like this (I’ve redacted my own values):

https://www.strava.com/oauth/token?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&code=1c49xxxxxxxxxxxxxxxxxxxxxxx&grant_type=authorization_code

If you’re using Postman, here is what the interface should look like:

Making that request will result in a response that looks like this:

{
"token_type": "Bearer",
"expires_at": 1681350948,
"expires_in": 21600,
"refresh_token": "25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"access_token": "87b0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"athlete": {
"id": xxxxxx,
"username": "xxxxxxx",
"resource_state": 2,
"firstname": "Spencer",
"lastname": "Attick",
"bio": null,
"city": "Oakland",
"state": "California",
"country": "United States",
"sex": null,
"premium": false,
"summit": false,
"created_at": "2015-04-01T00:46:54Z",
"updated_at": "2023-04-11T18:24:23Z",
"badge_type_id": 0,
"weight": 0.0,
"profile_medium": "https://graph.facebook.com/1226490129/picture?height=256&width=256",
"profile": "https://graph.facebook.com/1226490129/picture?height=256&width=256",
"friend": null,
"follower": null
}
}

The important fields to pay attention to here are expires_at, refresh_token, and access_token.

Strava’s access_token will expire at the expires_at time which is a Unix Epoch timestamp. We’ll talk about refreshing later, but for now let’s get some Strava data.

5. Get Workout Data from Strava
Alright! We can finally make a request to get the data we want from Strava!

Here you’ll want to make a GET request that looks like this:

curl --location 'https://www.strava.com/api/v3/athlete/activities' \\
--header 'Authorization: Bearer '

You’ll use the access_token you received above as the Bearer Token here. Ensure that you don’t have a request body at all for this step as that will throw an error.

Check out the response to that request. You’ve got Strava data you can work with! You can add this to your personal website, create a workout dashboard for yourself, create a leaderboard with friends, or whatever else you can think of!

7. Manage Refreshing the Access Token
The last thing we want to take care of is making sure you can refresh your token when it expires. To do this, you’ll want to keep track of the last expires_at field you received. When the current time is greater than the value of the expires_at value, it’s time for a new access_token.

You’ll make this POST request to get a new token:

curl --location --request POST 'https://www.strava.com/oauth/token

Here are the query params you’ll use and their values:

client_id: you can get this from your Strava account
client_secret: you can get this from your Strava account
code: you should have received this in the last step from the URL
grant_type: set this to refresh_token
refresh_token: the refresh_token from your last successful authentication request

Here is the full request with the query params in place:

?client_id=xxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxx&grant_type=refresh_token&refresh_token=25bd1cf38exxxxxxxxxxxxxxxxxxxxx'

The response will look like this:

{
"token_type": "Bearer",
"access_token": "6e6e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_at": 1681430228,
"expires_in": 21600,
"refresh_token": "25bdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Now you’re all set! You can make requests to Strava to get meaningful data on your activities and you can generate a new access token!

From here, you’ll want to put everything together into something you can use in your server. Here is the code I use for Strava integration that I wrote for my personal website:

const isStravaTokenExpired = (currentExpirationTime) => {
const currentEpochTime = Date.now();
if (currentExpirationTime === 'undefined') {
return \`There is an error with the currentEpirationTime, it's value is: \${currentExpirationTime}.\`
}
return currentEpochTime > currentExpirationTime;
}

const generateNewToken = async () => {
console.log('Generating new token...');
const requestOptions = {
method: 'POST',
redirect: 'follow'
};
const requestURL = \`https://www.strava.com/oauth/token?client_id=\${process.env.STRAVA_CLIENT_ID}&client_secret=\${process.env.STRAVA_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=\${process.env.STRAVA_CACHED_REFRESH_TOKEN}\`;

try {
let response = await fetch(requestURL, requestOptions);
response = await response.json();
if (response.message === 'Bad Request') {
console.log(response);
return;
}
return {
refreshToken: await response.refresh_token,
expirationTime: await response.expires_at,
accessToken: await response.access_token
}
} catch (error) {
console.log(error);
}

}



const persistNewTokenData = async (newTokenData) => {
// Read the .env file
const envBuffer = fs.readFileSync('.env');
const envConfig = dotenv.parse(envBuffer);

// Update the relevant key with the new value
envConfig['STRAVA_EXPIRATION_TIME'] = newTokenData.expirationTime,
envConfig['STRAVA_CACHED_REFRESH_TOKEN'] = newTokenData.refreshToken,
envConfig['STRAVA_CACHED_TOKEN'] = newTokenData.accessToken

// Write the updated key-value pair to the file
const envText = Object.keys(envConfig).map(key => \`\${key}=\${envConfig[key]}\`).join('\\n');
await fs.promises.writeFile('.env', envText);
};



const getStravaActivityData = async () => {
console.log('Requesting activity data from Strava...');
let myHeaders = new Headers();
myHeaders.append("Authorization", \`Bearer \${process.env.STRAVA_CACHED_TOKEN}\`);

const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};

try {
const response = await fetch("https://www.strava.com/api/v3/athlete/activities", requestOptions);
return response.json();

} catch (error) {
console.log('error', error)
}
}

//check to see if the expiration time has passed
const executeStravaLogic = async () => {
const isTokenExpired = isStravaTokenExpired(process.env.STRAVA_EXPIRATION_TIME);

if (typeof isTokenExpired === 'string') {
console.log('Please resolve the error with the current expiration time stored in the .env file.');
return;
} else if (isTokenExpired) {
console.log('The expiration time has passed. Generating a new token...');
//if yes - generate a new token
const newTokenData = await generateNewToken();
if (!newTokenData.expirationTime) {
console.log('There was an error getting refresh token data.')
return;
}
//save the new token info to .env
persistNewTokenData(newTokenData);

}
//make request to Strava activities endpoint
const stravaActivityData = await getStravaActivityData();
return stravaActivityData;
};

app.get('/api/strava', executeStravaLogic);

Feel free to use what works for you and scrap what doesn’t.

Enjoy!

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


Integrating with the Strava API was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

`,media:{}},{id:"https://medium.com/p/9778ad618cb4",title:"A Brief Introduction to Object Oriented Programming",link:"https://levelup.gitconnected.com/a-brief-introduction-to-object-oriented-programming-9778ad618cb4?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1682427096e3,created:1682427096e3,category:["javascript","object-oriented","technology","software-development"],content:`

What is it?

Object oriented programming (OOP) is a commonly used programming model that focuses on the use of classes and objects to create a codebase that is simple and reusable. It centers around the following concepts:

  • Abstraction
  • Polymorphism
  • Inheritance
  • Encapsulation

What is it used for?

By focusing on creating reusable pieces, OOP limits code duplication, makes code easier to read, and allows for the separation of concerns.

Due to its benefits and flexibility OOP can be used in nearly any type of project.

As it is so common and useful a framework, it’s generally something prospective employers will look for knowledge of either in a take home challenge or as a direct interview question.

What does it look like?

Without object oriented programming, you might write a piece of code in Javascript that looks like this:

The above example simply instantiates variables for different aspects of a dog and the declares a function to make use of one of those variables.

There are a few problems with this in that the variables are globally scoped leaving them open to collision or accidental manipulation, the pieces of code are not connected in any meaningful way — they’re all just hanging out in the global scope, and the code isn’t reusable — it has specific values for the declared variables which makes this pretty useless outside of describing a single dog.

We can make a big change to all of that without writing too much additional code.

Here is a refactor towards object oriented programming:

If we focus on the image to the left, we can see that the previous code which was loosely defined is now housed together in a single structure. This makes the code more readable in that we can easily tell that it’s all meant to be related.

The variables and function are now scoped to the class that was created which ensures less opportunity for collision or for them to be accidentally changed.

Additionally, the class we’ve created is now reusable. It isn’t limited to a single dog but is now broad enough to be able to accommodate any animal we’d like to supply. We can see this in action in the image on the right where dog and cat constants are declared and the class function is being made use of for each.

Much better, right?

Class Syntax in Javascript

I know you’re dying to get to the principles of object oriented programming, and we will get there, but first let’s take a closer look at the syntax for creating a class in Javascript.

Here we have a Bike class (it’s conventional to use camel casing for class names with the first letter also capitalized).

We initialize with the class keyword, the name of the class (in this case, Bike), and then curly braces {} .

From there, we create a constructor which holds the variables with which you can instantiate new instances of your class. In this case, those variables are make, model, and color. For example, you can create new instances of Bike using different values:

const firstBike = new Bike('Trek', 'Madone', 'Red');

const secondBike = new Bike('Bianchi', 'Oltre', 'Black');

After the constructor, functions can be declared. Note that the syntax here doesn’t use the function keyword. You can just go ahead and give your function a name and you’re rolling.

The Four Principles Of OOP

Ok, now we know what object oriented programming looks like and why it’s useful. Let’s move onto some OOP terminology that you can throw out in an interview to get the job and that you should keep in mind as you’re writing clean, reusable code.

1. Encapsulation
We’ve discussed this concept already, so let’s put a name to it. Encapsulation refers to the grouping of related functions and properties together to connect concepts and create DRY (do not repeat yourself) code.

Remember when we looked at the block of code where everything was declared in the global scope? Encapsulation fixes the problems that can bring by housing related code in one data structure (be that a class or an object):

2. Abstraction
We haven’t discussed this much yet but the concept of abstraction means to hide the complexity of your code or not allow the alteration of certain fields. You can think of this like any mechanical device (ex. a radio, microwave, TV, etc.). When you want to turn any of those things on, there is a mechanism to do so (ex. push a button). The actual mechanical functions that are performed to turn the appliance on within the device are not known to most of us and, frankly, we don’t much care as long as the thing turns on. The user has those processes obscured by the simplicity of the button push. That concept can also be applied to object oriented programming as abstraction.

Abstraction can be achieved in code through getters and setters which allow a user to only access or update certain fields in structured ways. We won’t talk much about how those work specifically here but managing how a user can see and make changes to the fields in your code makes a big difference in safeguarding the functionality of what you’ve written.

In the above example, we can see that the Shape class on the left has no access methods which encourages direct manipulation of the constructor. In contrast, the example on the right has specific methods that are written to get and set the number of sides a shape might have. The functionality is a bit simplistic for the sake of explanation there, but you can also add in checks to ensure things like the number of sides provided is actually a number and not another datatype.

3. Inheritance
Inheritance simply refers to child classes having the attributes of their parent classes passed down to them. This allows a programmer to write less code since methods that have already been written for a parent class don’t need to be rewritten for a child class to have access to them.

In the example above, a Person class is declared on the left with a method called introduceYourself() included on it. We can see on the right that a Student class is created which extends the Person class. This means that all of the functionality that the Person class has, the Student class also now has which we can see in action with the sayNameAndGrade() method on the Student class which makes use of the Person class’s introduceYourself() method without redefining it.

4. Polymorphism
Within the concept of polymorphism, the functionality of a child/parent chain of classes is determined at runtime. An example of polymorphism could be the method of a child class overriding the same method on a parent class. As the name suggests, through polymorphism object functionality can take “many forms”. The interaction between parent and children classes can be as simple as the child class inheriting functionality from the parent class or it can be more complex with overlapping methods or child functionality overriding that of the parent. This complexity is called polymorphism and it gives a developer more options in terms of what choices to make with the many available tools a chain or family of classes provides.

We can see a simple example above with the code on the left defining a Shape class. That class has a method on it called calcArea(). On the right, we see that Rectangle and Triangle classes extend the Shape class. These two child classes also each have a calcArea() method. When we create a Triangle instance and call calcArea() on it, it’s not the Shape's calcArea() function that is invoked, but the Triangle's. This is a simple example, but it does illustrate the flexibility inherent in working with classes that we call polymorphism.

Alright! That’s it! The last bit of wisdom I’ll leave you with is a way to remember object oriented programming terminology so you can always be ready to write better code or to ace an interview:

Abstraction

Polymorphism
Inheritance
Encapsulation

Additional Resources

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


A Brief Introduction to Object Oriented Programming was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

`,enclosures:[],content_encoded:`

What is it?

Object oriented programming (OOP) is a commonly used programming model that focuses on the use of classes and objects to create a codebase that is simple and reusable. It centers around the following concepts:

  • Abstraction
  • Polymorphism
  • Inheritance
  • Encapsulation

What is it used for?

By focusing on creating reusable pieces, OOP limits code duplication, makes code easier to read, and allows for the separation of concerns.

Due to its benefits and flexibility OOP can be used in nearly any type of project.

As it is so common and useful a framework, it’s generally something prospective employers will look for knowledge of either in a take home challenge or as a direct interview question.

What does it look like?

Without object oriented programming, you might write a piece of code in Javascript that looks like this:

The above example simply instantiates variables for different aspects of a dog and the declares a function to make use of one of those variables.

There are a few problems with this in that the variables are globally scoped leaving them open to collision or accidental manipulation, the pieces of code are not connected in any meaningful way — they’re all just hanging out in the global scope, and the code isn’t reusable — it has specific values for the declared variables which makes this pretty useless outside of describing a single dog.

We can make a big change to all of that without writing too much additional code.

Here is a refactor towards object oriented programming:

If we focus on the image to the left, we can see that the previous code which was loosely defined is now housed together in a single structure. This makes the code more readable in that we can easily tell that it’s all meant to be related.

The variables and function are now scoped to the class that was created which ensures less opportunity for collision or for them to be accidentally changed.

Additionally, the class we’ve created is now reusable. It isn’t limited to a single dog but is now broad enough to be able to accommodate any animal we’d like to supply. We can see this in action in the image on the right where dog and cat constants are declared and the class function is being made use of for each.

Much better, right?

Class Syntax in Javascript

I know you’re dying to get to the principles of object oriented programming, and we will get there, but first let’s take a closer look at the syntax for creating a class in Javascript.

Here we have a Bike class (it’s conventional to use camel casing for class names with the first letter also capitalized).

We initialize with the class keyword, the name of the class (in this case, Bike), and then curly braces {} .

From there, we create a constructor which holds the variables with which you can instantiate new instances of your class. In this case, those variables are make, model, and color. For example, you can create new instances of Bike using different values:

const firstBike = new Bike('Trek', 'Madone', 'Red');

const secondBike = new Bike('Bianchi', 'Oltre', 'Black');

After the constructor, functions can be declared. Note that the syntax here doesn’t use the function keyword. You can just go ahead and give your function a name and you’re rolling.

The Four Principles Of OOP

Ok, now we know what object oriented programming looks like and why it’s useful. Let’s move onto some OOP terminology that you can throw out in an interview to get the job and that you should keep in mind as you’re writing clean, reusable code.

1. Encapsulation
We’ve discussed this concept already, so let’s put a name to it. Encapsulation refers to the grouping of related functions and properties together to connect concepts and create DRY (do not repeat yourself) code.

Remember when we looked at the block of code where everything was declared in the global scope? Encapsulation fixes the problems that can bring by housing related code in one data structure (be that a class or an object):

2. Abstraction
We haven’t discussed this much yet but the concept of abstraction means to hide the complexity of your code or not allow the alteration of certain fields. You can think of this like any mechanical device (ex. a radio, microwave, TV, etc.). When you want to turn any of those things on, there is a mechanism to do so (ex. push a button). The actual mechanical functions that are performed to turn the appliance on within the device are not known to most of us and, frankly, we don’t much care as long as the thing turns on. The user has those processes obscured by the simplicity of the button push. That concept can also be applied to object oriented programming as abstraction.

Abstraction can be achieved in code through getters and setters which allow a user to only access or update certain fields in structured ways. We won’t talk much about how those work specifically here but managing how a user can see and make changes to the fields in your code makes a big difference in safeguarding the functionality of what you’ve written.

In the above example, we can see that the Shape class on the left has no access methods which encourages direct manipulation of the constructor. In contrast, the example on the right has specific methods that are written to get and set the number of sides a shape might have. The functionality is a bit simplistic for the sake of explanation there, but you can also add in checks to ensure things like the number of sides provided is actually a number and not another datatype.

3. Inheritance
Inheritance simply refers to child classes having the attributes of their parent classes passed down to them. This allows a programmer to write less code since methods that have already been written for a parent class don’t need to be rewritten for a child class to have access to them.

In the example above, a Person class is declared on the left with a method called introduceYourself() included on it. We can see on the right that a Student class is created which extends the Person class. This means that all of the functionality that the Person class has, the Student class also now has which we can see in action with the sayNameAndGrade() method on the Student class which makes use of the Person class’s introduceYourself() method without redefining it.

4. Polymorphism
Within the concept of polymorphism, the functionality of a child/parent chain of classes is determined at runtime. An example of polymorphism could be the method of a child class overriding the same method on a parent class. As the name suggests, through polymorphism object functionality can take “many forms”. The interaction between parent and children classes can be as simple as the child class inheriting functionality from the parent class or it can be more complex with overlapping methods or child functionality overriding that of the parent. This complexity is called polymorphism and it gives a developer more options in terms of what choices to make with the many available tools a chain or family of classes provides.

We can see a simple example above with the code on the left defining a Shape class. That class has a method on it called calcArea(). On the right, we see that Rectangle and Triangle classes extend the Shape class. These two child classes also each have a calcArea() method. When we create a Triangle instance and call calcArea() on it, it’s not the Shape's calcArea() function that is invoked, but the Triangle's. This is a simple example, but it does illustrate the flexibility inherent in working with classes that we call polymorphism.

Alright! That’s it! The last bit of wisdom I’ll leave you with is a way to remember object oriented programming terminology so you can always be ready to write better code or to ace an interview:

Abstraction

Polymorphism
Inheritance
Encapsulation

Additional Resources

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


A Brief Introduction to Object Oriented Programming was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

`,media:{}},{id:"https://medium.com/p/17f6e29a07ba",title:"A Guide to Immediately Invoked Function Expressions (IIFE)",link:"https://medium.com/@spencer.attick/a-guide-to-immediately-invoked-function-expressions-iife-17f6e29a07ba?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1681325789e3,created:1681325789e3,category:["javascript","security","code","software-development","technology"],content:'
Anne Nygård for Unsplash

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log("Hello, I am a self-invoking arrow function!");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = \'secret\';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log("Returning cached data");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log("Fetching data from server");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

',enclosures:[],content_encoded:'
Anne Nygård for Unsplash

What is an IIFE?

An immediately-invoked function expression works just the way it sounds — it is a function that is run immediately after it is declared.

The syntax looks like this:

(() => {
console.log("Hello, I am a self-invoking arrow function!");
})();

Running a file containing an IIFE demonstrates that the tradition syntax that is generally needed to call the function isn’t used. The function just runs where it’s been declared:

Ok, but when should I use this?

There are a few reasons why you need to know about IIFEs as a Javascript developer. Here are the top four:

1. Creating a private scope: When you define variables inside an IIFE, they are not accessible outside of the function. This helps to prevent naming conflicts with other variables in your code. This is useful when you want to create a module or library that can be used in different parts of your application without worrying about variable conflicts. Here is an example of private scope in an IIFE keeping a value secured within the function scope:

const myModule = (() => {
const privateValue = \'secret\';

const publicMethod = () => {
console.log(`The private value is ${privateValue}`);
};

return {
publicMethod: publicMethod
};
})();

myModule.publicMethod(); // Output: The private value is secret
console.log(myModule.privateValue); // Output: undefined

2. Avoiding global variables: Global variables can be accessed from anywhere in your code and can lead to naming conflicts and unexpected behavior. By wrapping your code in an IIFE, you can keep your variables and functions local to the IIFE and avoid polluting the global namespace.

3. Running code immediately: Sometimes you might have a block of code that needs to be executed immediately when your script is loaded. By wrapping that code in an IIFE, you can ensure that it runs immediately without having to call a separate function.

4. Caching values: If you have a value that is expensive to compute or retrieve, you can use an IIFE to cache the value and use it multiple times without recomputing or retrieving it.

Here’s an example of an IIFE that implements a simple caching mechanism using closure:

const getData = (() => {
let cache = {};

const getDataFromServer = async (url) => {
// Make an AJAX request to the server to get the data
const response = await fetch(url);
const data = await response.json();

// Cache the data for future requests
cache[url] = data;
return data;
};

return async (url) => {
if (cache[url]) {
// If the data is already in the cache, return it
console.log("Returning cached data");
return Promise.resolve(cache[url]);
} else {
// If the data is not in the cache, get it from the server and cache it
console.log("Fetching data from server");
return getDataFromServer(url);
}
};
})();

// Use the getData function to fetch data from a URL
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

// The second time we call the function with the same URL, it will return the cached data
getData("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data));

In this example, we are using an IIFE to create a function called getData, which implements a simple caching mechanism for fetching data from a server. The function uses closure to store a private cache object, which is used to cache the data for future requests.

When the getData function is called with a URL, it checks if the data is already in the cache. If it is, it returns the cached data immediately. If not, it makes an AJAX request to the server to get the data, and then caches it for future requests.

By using an IIFE to create the getData function, we can ensure that the cache object is only accessible within the function, and cannot be modified or accessed by external code. This can help improve the reliability and performance of our application by avoiding unnecessary requests to the server and reducing network traffic.

What do I need to know about IIFE with ES6?

IIFEs have been a part of JavaScript since the beginning, and can be used in all versions of the language.

However, ES6 did introduce a new way to create blocks of code with their own scope, called “block scoping”. Block scoping can be achieved using the let and const keywords, which allow you to define variables with block scope rather than function scope.

So while self-invoking functions are not specific to ES6, the concept of creating local scopes has been enhanced by ES6 with the addition of block scoping.

This technique is often used to create a local scope for variables and functions, as the variables and functions defined inside the IIFE are not accessible outside of it.

Have you found an innovate use case for IIFEs? If so, I’d love to hear about it!

',media:{}},{id:"https://medium.com/p/c3214c897b4c",title:"Project: Script to Automatically Update Resources in my Portfolio",link:"https://medium.com/@spencer.attick/project-script-to-automatically-update-resources-in-my-portfolio-c3214c897b4c?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1681220946e3,created:1681220946e3,category:["portfolio","nodejs","software-development","javascript","script"],content:'

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

',enclosures:[],content_encoded:'

As I fine-tuned the code I had written to power my portfolio, I ran into a problem with hosting. I had originally wanted to host on Github Pages which ended up being easy to get set up with but which I quickly learned would serve static assets only and would not be able to run my server file. I had been excited about the prospect of having live data on my site pulled from Medium and Goodreads so running into this limitation this was a bit of a set back. It is important to me to be able to pull in realtime data to showcase my up to date blog posts, projects breakdowns, and books that I’m reading in my portfolio and that wasn’t functionality I was willing to give up.

The Problem

I looked around for another hosting solution but was unable to find anything that didn’t require a credit card to be put down up front. As a strict money conscience individual, that didn’t fit what I was looking for to host a small project like my portfolio.

I didn’t want to have to update my resources manually for obvious reasons but I still wanted to make sure viewers to my portfolio were getting an up to date experience.

The solution I landed on was to write a script that could clone my portfolio’s code onto my desktop, make requests to ask Medium and Goodreads for my RSS feeds, add that data into my portfolio, and push the results back up to Github when it was run. That way, fresh data would be served just by me running a single terminal command.

This wasn’t as clean a solution as finding hosting service that would read my server file and make outgoing requests itself, but it was going to give me the opportunity to learn a few new things in the form of writing such a script.

The Work

To get started here, I first need to learn about the child_processes module in Node.js (this resource was really helpful). With that knowledge in hand, I was able to use the exec() function as well as the Node.js File System module to look for my portfolio on my desktop, git clone it if it didn’t exist, make the outgoing requests for new data, add the updated data to the project, and then git push the updated files to Github. From there, Github Pages served that new data.

This process required me to have static assets holding the RSS returns (which I parsed to JSON) from both Medium and Goodreads. To get that data compiled through Vite, I also had to run the npm run build command in my script before pushing to Github.

Another consideration here was formatting in terms of the logging I was doing. As this script impacts my portfolio (which I’d, of course, always want to be in working condition), I added fairly robust logging to ensure that I could easily see which parts of the script were running and what errors, if any, were surfacing. This resulted in quite a few lines being printed to the console. Keeping everything organized was a small challenge and one I met with console coloring through ASCII.

Finally, I wanted to add testing into the mix as I was making changes to my portfolio and pushing them automatically to Github. As I was coding my project I was repeatedly sending unnecessary updates to Github to test functionality when I should have had a test file in place that I could run instead of making production runs of the script. To resolve this, I created a Mocha test file and added a test suite there to ensure the functionality of all the different aspects of my code.

With all of that in place, making quick updates has been incredibly easy. I run my script once a day (which takes less than a second) and my portfolio is refreshed with up to date information that I can rely on. I still hope to formally host the whole project, including the server, but while I’m shopping around for good options, I have this in place so as to not lose out on my portfolio’s functionality.

The Code

The code for this project can be found here: https://github.com/spencerattick/staticResourcesPortfolioScript.

What I Learned

I had a lot of fun coding this (hopefully) temporary solution to a blocker that I was running into! I got to learn about child_processes and console coloring in terms of new technical skills gained. I also got a refresher on why it’s important to start with tests in place or write them out as the project progresses so as to not have to rely on running the project over and over to catch bugs and gain verification. I’ll definitely be writing my tests sooner next time.

',media:{}},{id:"https://medium.com/p/a4c2e35b2558",title:"Project: Creating Code Examples for Segment’s Server-Side Libraries",link:"https://medium.com/@spencer.attick/project-creating-code-examples-for-all-of-segments-server-side-libraries-a4c2e35b2558?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1680791081e3,created:1680791081e3,category:["python","segment","servers","ruby","nodejs"],content:'

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

',enclosures:[],content_encoded:'

As a tool for ingesting data from many disparate locations to be piped to a large suite of downstream tools, Segment requires the flexibility to be easily used in as many codebases as possible. As such, their product is supported in a number of libraries built to accommodate use in different languages.

That being the case, Segment’s Success Engineering team is tasked with being able to debug issues customers might have with each of those libraries. Many folks on the team are deeply familiar with one or two of the supported languages but there were a few most folks didn’t know and some that no one has experience with.

The Problem

As a result, debugging customer issues with some of these languages could take an inordinate amount of time. Just getting a test project set up in some of these languages can be tricky and take quite a while if someone is doing it for the first time or hasn’t done it in a while. Even setting up something fairly simple that one does know well takes at least a few minutes. All of the time getting set up was causing frustration among the Success Engineering team, bringing down the number of simultaneous tickets any one person could be working on, and creating a situation where the customer was waiting to get unblocked for much longer than need be.

Half the time, reproducing the customer’s issue wasn’t the problem, just understanding how to set up a Java or PHP project or working with another language less familiar to the team and figuring out how to add Segment to it was the blocker.

Segment’s public documentation has detailed instructions for how to add Segment to a project but not on how to spin up a project in the first place. Depending on an individual’s level of knowledge, it can be tricky to get set up with Segment in a tool you’re unfamiliar with.

The Work

Realizing this glaring problem that was preventing myself and my colleagues from quickly reproducing customer issues to help them be able to move forward, I decided I needed to create a one-stop-shop solution to be able to spin up a project in any of the languages Segment supported without getting blocked by not being familiar them.

To this end, I’d worked with Replit before and decided I would start there. Their platform allows users to spin up entire projects completely in the browser without needing to install anything onto a machine. Projects created there can also be forked and used by others without the need for package installation or any other setup. This seemed like the perfect place to house projects that could mock Segment implementations in various languages without Success Engineers getting stuck on the logistics of setting something up themselves.

I was able to get about half of Segment’s server-side libraries to work with Replit such that Success Engineers on my team could go ahead and fork the project, make any small changes they needed for their own testing (although the projects will run with just the push of a button at that point), and then proceed with their debugging. This saved them the often time consuming task of having to get anything set up locally, worrying about what to install and where, and many of the other nuances that can come up when using a new tool.

Once I wrote the necessary configuration code for each language and installed the requisite packages, everything worked out of the box for my team.

There are a few Segment libraries that Replit either doesn’t support the languages for at all such as .NET or for which other limitations came into play such as Replit not allowing outside packages to be loaded in with Clojure.

In those cases, I worked to spin up instances of each library locally and provided detailed instructions to help my teammates get up and running as quickly as possible. I found that this took a lot of the guesswork and Googling out of the setup process for folks (and for myself when I came back to a library after weeks or even months of not needing to use it).

When Replit wasn’t available, having those instructions on hand hugely sped up the debugging process for my team.

While this was a project I spearheaded, I did also get to collaborate with my colleagues on it. A couple of the library installation instructions were either added or enhanced by teammates who were inspired to help ensure all of Segment’s libraries had a guide to get our team quickly up and running whenever necessary.

In addition to providing Replit projects and/or setup instructions for each library, this initiative also ended with a very comprehensive and living document where folks can continue to add new learnings (ex. how to add logging or specific syntax) so that anyone needing to use additional features wouldn’t be blocked.

After this was all up and running I went through all of Segment’s libraries (client and server-side) to create a second document that housed syntax for adding the context object which is an optional section of information that can be added to each payload. It isn’t always straightforward to figure out how to add it if you’re not familiar with a particular language so I wanted to make sure folks had that resource on hand as well.

These technical documents have really sped up the time to resolution for our customers, have driven down the amount of time Success Engineers need to spend spinning up a project in a certain language, and have allowed my team to navigate tickets regarding Segment libraries with much more ease.

The Code

Each link here will take you to the Replit for each implementation. Please note that these project instances are actively used for testing and may look a little messy as a result since we’re running different configurations on the fly to get customers their answers as quickly as possible.

Java

Python

Ruby

Go

Node.js

Unfortunately, I can’t share more here as the rest of the document is housed as part of Segment’s internal resources.

What I Learned

This project was a great way to get experience working (albeit on a small scale) with numerous different languages and tools. I was able to spend time with documentation for languages I’m not as familiar with and to get a sense of what each language requires. It was an awesome opportunity to get to write code in several different languages and to add great benefit to my team as a result.

',media:{}},{id:"https://medium.com/p/fc36c10c02e5",title:"Project: Story Points",link:"https://medium.com/@spencer.attick/project-story-points-fc36c10c02e5?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1680618193e3,created:1680618193e3,category:["javascript","support","optimization","zendesk","story-points"],content:'

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

From there, logic is in place to add points up based on which fields are used:

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

',enclosures:[],content_encoded:'

As a Success Engineer hoping to level up my engineering skills, I spoke to my manager at the time to get some ideas of what I could do that would be of benefit to the team. She shared an idea she’d had for a while of configuring a story points addition to the current metrics we held around taking and solving technical support tickets.

The Problem

No support ticket is created equal with some being substantially harder to debug and much more time consuming than others. To date, my team had mostly been using ticket volume per person to get a sense of everyone’s current workload. As tickets vary so much in difficulty, just using raw counts didn’t give managers a clear picture into what someone’s workload was like at any given time.

The team already collected a variety of data points for each ticket that gave a sense of context such as what Segment plan tier the customer was using, what topic they were asking about, and a few other things. With that information at hand, an educated guess could be made about how difficult an individual ticket would be.

My team uses Zendesk for support ticketing and we already had an app in place on that platform to collect the number of tickets each person was fielding at once. This was helpful, but didn’t provide a wholistic picture.

Enter story points.

At a high-level, story points are meant to assign a numeric value to the difficulty of a task relative to other, similar tasks which is useful in things like helping managers understand how to prioritize projects or, in our case, better understand individual workload.

With all that in mind, I was off to get this metric improvement figured out!

The Work

I knew this project would not only involve coming up with a programmatic solution to calculate story points per ticket, but would also require Zendesk and a custom server to communicate so that I could grab the metrics we were already tracking from Zendesk, send it off to my codebase, have the codebase come up with the appropriate numbers, and then send that data back to Zendesk so that it could be displayed in our existing ticket counter app.

To get started, I mulled over my hosting options, not wanting to complicate things too much or rely on a hosting platform that might need its own upkeep. I quickly decided to use some of my company, Segment’s, own functionality to write the code to power this project: Source Functions.

Source Functions collect data via webhook and then allow custom code to be written to alter or parse that data before Segment’s systems ingest it into its traditional pipeline. From there, that altered data can be sent downstream to connected destinations. While Segment does have a pre-built connection to Zendesk as a destination already established, I didn’t feel it was right for my use case. More on that later.

After landing on a hosting option that actually didn’t require me to do much by way of setting the project up, I went ahead and configured Segment’s Zendesk instance to send requests to my Source Function. The information I sent over included all of the fields I felt would give me the best ability to determine a ticket’s difficulty. The most useful of these were plan tier, topic, and subtopic.

In terms of plan tier, larger customers generally have more a complex implementation and more access to other folks at Segment so their questions tended to be more involved than those of other users. As for topic and subtopic, some aspects of Segment’s pipeline and feature offerings (naturally) are more difficult to debug than others. Taking all of this into account gave me a fairly accurate ability to assign a number based on those factors to a ticket. Of course, I didn’t just trust my own instincts here, I reached out to my entire team to help with calibration.

I wanted to ensure transparency in terms of what point value I was assigning to each ticket so in my Source Function, I make a call back to Zendesk to add a field on each ticket to let the ticket holder know what point value has been assigned which looks like this:

As far as I know, Zendesk doesn’t have a field that works well to hold static values, so I left a note for my team not to edit that value. Though in terms of the way the process works, any update there wouldn’t have actually mattered and would have just been programmatically changed back down the line. I also shared the code that dictated the point values with my team so they could reference it if the number seemed higher or lower than they expected relative to the amount of time and effort they were spending a particular ticket. From there, I also put a Slack workflow in place to help folks report tickets where points didn’t align with the actual work that they were doing to help with team calibration such that the system could work as seamlessly as possible.

The code itself consists of objects containing all of the fields available in Zendesk and their point values (as determined by the team) depended on the level of difficultly generally associated with them:

From there, logic is in place to add points up based on which fields are used:

After that, a request is made into Segment’s pipeline with metadata so that the result of the process can be viewed and parsed in a downstream warehouse:

Additionally, an outgoing API request is made in the body of the Source Function to update Zendesk with the necessary data. This functionality goes outside of what Segment offers in its out-of-the-box integration with Zendesk so a custom request to Zendesk’s Tickets API was needed.

With all of the code and API functionality in place, the last piece of this project was to capture total points in aggregate for each individual which demonstrates how many points they have for all of their open, pending, and on-hold tickets combined:

This involved accessing an existing codebase my team maintains, getting a feel for how it works, and then adding to it to get the functionality pictured above.

The end product here is more visibility for managers and teammates around workload with more depth than just count of tickets. Being able to see who is most or least busy can help mitigate overwhelm by ensuring folks don’t take on more than they can handle or can offload some tickets to focus on others. It can also help managers quickly see who has a bit more capacity if an urgent or difficult case comes through the pipeline that need immediate attention.

The Code

Unfortunately, the full code for this project isn’t public as it concerns an internal Segment process and lives within my private Segment workspace. I’ve included screenshots above to give a general sense of what it looks like and what it does.

What I Learned

This project was really fun! It gave me some practice improving an internal process and getting different tools to talk to each other. This worked really well for the team and has been in place for a couple of years now. We recently rolled out some new functionality on the team and went ahead and copied the code for this project to put in place a similar metrics system it for a new part of our workflow.

The one piece of this that can’t be automated is adding in point values for new topics that come up as Segment’s pipeline grows. A person on the team will need to decide what point value to add based on how easy or difficult a topic is known to be. As such, I recently onboarded some newer team members to go through and make updates where that made sense. It felt great to get this project up and running, see its value over the past couple of years, and then to find others on the team who were interested in maintaining and hopefully building on what I originally put in place.

',media:{}},{id:"https://medium.com/p/f5efe0afd046",title:"Project: Updating Segment’s analytics-node Library to Include TypeScript",link:"https://medium.com/@spencer.attick/project-updating-segments-analytics-node-library-to-include-typescript-f5efe0afd046?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1678993785e3,created:1678993785e3,category:["segment","codecademy","node","typescript"],content:'

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

',enclosures:[],content_encoded:'

In working as a Success Engineer at Segment, I’ve gotten a chance to get very familiar with the integration libraries Segment offers in various languages. In looking for some proactive ways I could help out our customer-base, I noticed several folks asking for TypeScript support in Segment’s analytics-node library. Having never worked with TypeScript, I thought it would be a great opportunity to gain some experience with a technology new to me.

The Problem

A few customers had pointed out that they had run into issues using Segment’s analytics-node library that could have easily been avoided if the library had been refactored to use TypeScript. They pointed out that the addition would be fairly easy and wouldn’t take much time. Reading through requests for TypeScript that could have saved our customers time and frustration, I went ahead and took the project onto make the update.

The Work

The first step I took was to take Codecademy’s fantastic course on TypeScript to learn what it was all about. There I got practice with the syntax and requirements that TypeScript imposes on top of Javascript. The course also shared the reasons behind TypeScript and how to get started in using it more broadly.

From there, I forked Segment’s existing library to get a refresher on how it worked currently.

After getting the lay of the land, I consulted TypeScript’s documentation regarding migration from a Javascript project to one written in TypeScript. After all, I had just been learning in Codecademy’s pre-established environment and had yet to write any TypeScript in the wild. I found the documentation to be very straightforward and soon had the project configured correctly to throw TypeScript errors that I could then work through solving.

Before hopping into that error set, I saw immediately that there were some obvious changes I could make in terms of adding types to variables already listed at the top of the main file and taking on other small updates.

After making any necessary changes that stood out to me, I went back and forth running the TypeScript file and the existing test file that the Segment team had written to correct the code making sure to gradually reduce errors that cropped up in either place. I ran into many TypeScript file updates breaking the test file and vice versa which was a truly great learning experience complete with countless trips to Stack Overflow.

As I was acquiring a ton of new knowledge and had cleared out most of the errors that TypeScript had found and all of the ones that came up through Segment’s test suite, I became aware that Segment was actually writing a brand new library for Node and that the one I was working on would be relegated to maintenance mode. Upon learning that, I didn’t want to leave my project unfinished so I worked on cleaning up the last few errors before ultimately submitting all of my updates as a pull request. I was concerned they wouldn’t merge a change like this with the new library on the horizon so I did disable a few pieces of TypeScript functionality to get my pull request in before too much time elapsed. My hope was that I could continue to work on the project and clear those final errors if the team was interested in merging my code. When the team reviewed what I had written, they were appreciative but ultimately decided not to more forward with it in favor of the new library.

In the end, I got a lot of value from the opportunity my role gave me to find a gap like this, to learn the necessary technology, and to refactor existing code to meet the needs of Segment’s customers. Even though my code wasn’t merged it was a great learning experience.

The Code

Here is the pull request I made on the analytics-node library to incorporate TypeScript: https://github.com/segmentio/analytics-node/pull/356.

What I Learned

I got to learn TypeScript!

More importantly, I also learned that, though I was interested in working on a project just to help out and to practice a new skill, it would have been a good step to take to communicate with Segment’s libraries team about my interested in making the update. If I had, perhaps they would have been able to share that they were already working on something new and I could have seen if there was anything I could have helped with in that codebase instead.

Overall, I got some excellent experience in terms of technology and process that I can take with me onto whatever comes next!

',media:{}},{id:"https://medium.com/p/9251d1438121",title:"Project: Segment Source Function — Auth0",link:"https://medium.com/@spencer.attick/project-segment-source-function-auth0-9251d1438121?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1678737033e3,created:1678737033e3,category:["auth0","javascript","node","segment"],content:'

Project: Segment Source Function — Auth0

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

',enclosures:[],content_encoded:'

Project: Segment Source Function — Auth0

Segment Source Functions

As a Success Engineer at Segment, I’ve had great opportunities to practice and level-up my development skills. One of the ways I’ve used my coding skills to help our customers get set up as quickly as possible was to write a Function that met the needs of folks attempting to use a third-party tool and which removed the necessity for their teams to have to set up a file from scratch to accomplish the task they were interested in.

In the Segment ecosystem, a Function is a feature where custom code can be written and executed either before data formally enters the Segment processing pipeline (Source Functions) or at the end of the pipeline to push data out to non-Segment entities (Destination Functions). Folks will usually use Functions to send data to Segment from a source that isn’t supported by the product directly or to send data out to a destination that Segment doesn’t support out-of-the-box. The Function environment is highly customizable and can act as a stand in for part of a customer’s codebase.

Because the feature is so open-ended, Segment maintains an open source library to give customers a starting point for writing Source or Destination Functions for entities that are frequently asked about.

When I saw two separate requests come in from customers in one week asking for help to send data into Segment from Auth0 I decided to take on the project of creating a connector template that customers could use to set that connection up without having to start from scratch.

The Problem

Auth0’s platform had previously supported a direct integration to Segment but was in the process of deprecating that connection. If customers wanted to send data from Auth0 to Segment, the easiest way to do so would be via a Source Function.

While Segment’s ingestion API has a particular data format it requires, a Source Function can accept any configuration of data (as long as it’s JSON). That means that customers can send data to a Source Function from nearly anywhere and then write custom code in their Function to transform that data into something Segment’s API will accept.

As such, the project here involved looking at the data Auth0 could send out to a webhook and then to write code to make that data usable within Segment in a way that wouldn’t make too many assumptions. After all, this was meant to be a reusable file that customers could change to fit their individual needs.

The Work

In looking at the data Auth0 sent out in their logs to a webhook, I noticed that they didn’t include an event name that could be easily referenced. Data is much more usable if it hits Segment with a coherent event name that can be used to organize or analyze data in downstream tools (ex. Amplitude, Redshift, Mixpanel, Google Analytics, etc.). To this end, I found a guide that maps log codes (which Auth0 sends in their payloads) to more easily understandable event names. I used that to start to build a payload that could be sent to Segment. From there, I opted to send all of the other pieces of information from the Auth0 log to Segment as properties so that customers could start with the whole set but, of course, could pare things down if they decided they needed to.

Once the information to be added to the payload the was sorted, I noticed that the resulting requests could be quite large. I wanted to make sure that the event my code generated would be within Segment’s size parameters so I added some validation in to do that. If the payload was within Segment’s limit, my code would go ahead and send it on, if not, I opted to remove a section of the payload that I noticed often accounted for a size issue and didn’t seem like incredibly relevant data to hold onto. Of course, customers then had the template for doing this I wrote so they could easily decide to remove a different section based on what they found relevant. I left comments in my code to ensure these checks and the removal was obvious as Segment users come from all different backgrounds and some are more technical than others.

The Code

Segment’s teams are in the process of restructuring, so for now there is a backlog of pull requests on the Function’s library repo. That said, here is my pull request awaiting a time when it can be merged: https://github.com/segmentio/functions-library/pull/70.

I’ve been able to share that solution with customers who’ve asked about getting set up with Auth0 so they don’t have to start from scratch.

Overall, this has been a great (albeit small) opportunity to get some practice while helping out Segment’s customer base.

What I Learned

Through this project, it was reinforced for me that I don’t need to take on a large-scale project to make an impact. A smaller implementation with just around ~150 lines of code can be great practice and can really help our customers get started with what they’re hoping to do so they can spend less time writing custom code and more time getting their data pipeline dialed in. Since this Function template is reusable, customers from here on out can use it however they see fit in order to send their Auth0 logs to Segment.

',media:{}},{id:"https://medium.com/p/7f03b1b9e1b9",title:"Scope, Hoisting, and Closure in Javascript",link:"https://medium.com/@spencer.attick/scope-and-hoisting-and-closure-in-javascript-7f03b1b9e1b9?source=rss-e5dc359f27c2------2",author:"Spencer Attick",published:1675365832e3,created:1675365832e3,category:["javascript-tips","hoisting","javascript","scopes","closure"],content:`
Michael Surazhsky for Unsplash

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

`,enclosures:[],content_encoded:`
Michael Surazhsky for Unsplash

So you’ve learned how to execute functions and have maybe even written a few simple programs. Nice! Now it’s time to level up with some more advanced Javascript concepts. Understanding scope, hoisting, and closure are essential to internalizing how your programs will run and what behavior you can expect for certain situations that can really trip you up if you’re unclear on these concepts.

Let’s get into it!

Scope

The first of these concepts to understand is scope. When we’re talking about scope we’re referring to where in the program variables are accessible. There are four types of scope:

Global Scope

With global scope, variables are declared in such a way that makes them accessible to an entire file. To scope variables globally you just declare them outside of any functions or modules. This isn’t recommended as you might declare a global variable, forget about it, and then attempt to use the same variable in a different way elsewhere in you code which can create unexpected behavior and cause confusion, especially if you’re working with a team.

Here is an example of a variable defined with global scope:

// Define a global variable
let globalVariable = 'I am a global variable';

// Define a global function
function globalFunction() {
console.log(globalVariable);
}

// Call the global function from anywhere in the program
globalFunction(); // outputs: 'I am a global variable'

The globalVariable and globalFunction values can be referenced anywhere in the file in which they are instantiated (though there are some limitations around hoisting which we’ll discuss later).

In Javascript, each file you create has its own global scope. You can’t share information between files without modular scope.

Modular Scope

Modular scope refers to a process by which variables are encapsulated in a module (ES6 offers this functionality). That module can then be shared with other parts of your program. With modular scope, variables and functions can be accessed in their own module and any other places where that module is imported.

Here is an example:

// Define a module
module.exports = {
moduleVariable: 'I am a module variable',
moduleFunction: function() {
console.log(this.moduleVariable);
}
};
// Import the module into another file
const myModule = require('./myModule');

// Call the module function
myModule.moduleFunction(); // outputs: 'I am a module variable'

Block Scope

Block scope refers to anything contained between two curly braces {} . This includes things like variables created within a function or variables between curly braces inif statements.

Here in an example of block scoping:

if (true) {
let x = 10;
}
console.log(x); // ReferenceError: x is not defined

The x variable is blocked scoped (due to the use of let) and cannot be referenced outside of the curly braces it is contained in.

Please note here that var is function scoped (we’ll talk about this in just a second) but let and const are block scoped. I won’t go into that too much about var here, but in 2023 there are very few cases where it is needed so Javascript developers should be sticking with let and const .

Function Scope

Function scope and block scope are essentially the same thing unless you’re using the var keyword. It’s a best practice when using Javascript to stick to const and let so you don’t have to worry about function scope outside of the concept of block scope.

The difference when using var is that it makes variables accessible anywhere in the function outside of blocks, whereas let and const can only be accessed in the blocks in which they are defined.

We can see this illustrated in the example below:

function functionScope() {
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // 10
console.log(y); // ReferenceError: y is not defined
}

functionScope();

In this example, both x and y are declared within the block of the if statement, but x is declared using the var keyword, while y is declared using the let keyword.

Since var has function scope, it can be accessed outside of the block, and its value is logged to the console as 10. However, let has block scope, which means it is only accessible within the block so trying to access it outside will result in a ReferenceError.

Hoisting

Now that we’ve talked a bit about scope, let’s work on understanding the concept of hoisting. Essentially, hoisting takes some of your code and moves it to the top of its scope before the file is executed automatically with Javascript.

What get’s hoisted?

Function declarations (functions created using the function keyword) are hoisted in their entirety. Consider this example and feel free to run it on your end:

thisGetsHoisted(); //HOISTED!


function thisGetsHoisted() {
console.log('HOISTED!');
}

The function is called before it is declared, but it still works! The reason for this is that the Javascript engine automatically hoists function declarations to the very top of the scope that the function occupies at runtime. Because of that, it has already stored thisGetsHoisted in memory such that it can be accessed before the file actually gets run.

Be careful though because the same thing will not happen with anonymous functions instantiated with let or const.

thisGetsHoisted(); //ReferenceError: Cannot access 'thisGetsHoisted' before initialization


const thisGetsHoisted = () => {
console.log('HOISTED!');
}

The let and const keywords are not hoisted in the same way as function declarations. Space is also made for them in memory, but no value is assigned by Javascript which means a ReferenceError is thrown. Using anonymous functions with let and const helps you avoid the tricky behavior that can come about with hoisting in that it forces you to write your code such that functions are created in space before they are run.

We see the same thing if we attempt to access a variable rather than a function:

console.log(isntHoisted); //ReferenceError: Cannot access 'isntHoisted' before initialization

let isntHoisted = 'nope';

The var keyword has different behavior here (you knew it was coming). The Javascript engine will also create space in memory for var variables before running a file but instead of not assigning a value for those variables, Javascript will set them to undefined . Behaviorally, this means you still can’t use them before you initialize them (set a variable for them) in space, but you won’t get a ReferenceError if you try. Instead, the variable will just be treated as what it is, undefined:

console.log(isntHoisted); //undefined

var isntHoisted = 'nope';

Essentially, all variable and functions are hoisted to the top of their perspective scopes, but only function declarations get hoisted with their values (the functions themselves). The let and const keywords allow their variable keys to be hoisted, but the values they hold won’t be. Those variables also won’t be assigned a value at all in the hoisting process. As we’ve seen, var keys will also be hoisted but they’ll be set to undefined until the file is run and a value is assigned to them in the same way as let and const . The span of time between when a variable is hoisted and when it is initialized (given a value) is called the temporal dead zone.

Another thing to know about hoisting is that it can slow down your program as extra work needs to be accomplished before your file will be executed in the form of pulling all of those variables and function declarations to the top of the page. To limit this slowdown, it is advised to use let, const, and anonymous functions so that less information needs to be stored in memory before a file runs. The slowdown here is generally pretty minimal but it’s still something to keep in mind. It’s also best practice to limit the scope of variables as much as possible so they can get removed from memory (garbage collected) when they aren’t needed anymore rather than having them hang around in the memory heap indefintiely. This can take the form of scoping variables to modules or functions rather than creating variables in the global scope.

Closure

The last Javascript concept we’ll look at is closure. Closure refers to a function that has access to variables in an outer function, even after the outer function has returned. The values associated with the outer function are held in memory even after the out function is called. That was a lot, so let’s look at an example.

Here we have a function which returns another function:

Note that the inner function, secondFunction, returns a console.log of the arguments of both secondFunction and firstFunction.

On line 10, a call to firstFunction is made with 1 passed in as an argument. We might expect that value of 1 to be available only on line 10 as that is where the function call is made with 1 as an argument. Notice that the call to secondFunction isn’t made (where the arguments for both functions are meant to be printed out) until line 14.

This is where closure comes in!

In this example, secondFunction has closure over over firstFunction meaning that secondFunction not only has access to its own scope, but also the scope of the outer function (firstFunction). This includes any context firstFunction has even after it’s executed - which is pretty magical.

Keep in mind that closure only works if you’re calling the inner function directly. If you attempt to log out a variable in the outer function without calling the inner function, that won’t work:

Notice on line 14, instead of calling a function, I’m just trying to log out first which is only scoped to firstFunction and (because of closure) secondFunction. The first variable cannot be referenced alone in the global space as first isn’t a globally scoped variable. It’s function (or block) scoped.

Scope, hoisting, and closure are more advanced Javascript topics so I’d encourage you to continue to seek out examples and practice with them to really internalize what’s happening with your code in terms of each concept. I’ve shared some resources below that are a good next step for anything you’d like more information on.

References

Hoisting

https://www.youtube.com/watch?v=EvfRXyKa_GI

https://www.youtube.com/watch?v=_uTDzYyYz-U

https://www.youtube.com/watch?v=j-9_15QBW2s

https://www.youtube.com/watch?v=ppMlvGMT2qE

Scope

https://www.youtube.com/watch?v=TkFN6e9ZDMw

https://www.youtube.com/watch?v=bD-62OMzni0

https://www.youtube.com/watch?v=ppMlvGMT2qE

Closure

https://www.youtube.com/watch?v=3a0I8ICR1Vg&t=27s

https://www.youtube.com/watch?v=vKJpN5FAeF4

https://www.youtube.com/watch?v=1S8SBDhA7HA

`,media:{}}],Gh={title:V1,description:B1,link:$1,image:G1,category:Y1,items:q1};function Q1(){const[e,t]=se.useState([]),[n,r]=se.useState(null),a=s=>{let l=s.split("<"),c,d;for(const p of l)if(p.includes("cdn-images")){c=p;for(const m of c.split("="))if(m.includes("cdn-images")){d=m.split(" ")[0];break}return d.replace(/"/g,"")}},o=s=>{const l=s.toString().slice(0,-3),d=new Date(l*1e3).toDateString().split(" ");return`${d[1]} ${d[2]} ${d[3]}`},i=s=>{let l=[];for(let c of s){if(l.length===6)return l;c.title.includes("Project")||l.push(c)}return l};return se.useEffect(()=>{async function s(){try{const c=await(await fetch("http://localhost:3000/api/posts")).json();t(i(c))}catch(l){console.log(l),t(i(Gh.items))}}s()},[]),n?ie("div",{children:["Error: ",n.message]}):ie("div",{id:"blog",children:[C("h2",{children:"Blog"}),C("div",{id:"blog-posts",children:e.length>1?e.map(s=>{const l=a(s.content),c=o(s.created);return C("div",{className:"post",children:ie("a",{href:s.id,target:"_blank",children:[C("img",{src:l,alt:s.title}),C("p",{className:"date",children:c}),C("p",{children:s.title})]})},s.id)}):ie("div",{children:["It looks like something went wrong. You can check out my blog here: ",C("a",{href:"https://medium.com/@spencer.attick",children:"Spencer's Blog"}),"."]})})]})}const J1="/assets/SpencerAttickPortfolio-03c16df4.pdf",K1=[{method:"email",icon:M1,link:"mailto:spencer.attick@gmail"},{method:"linkedin",icon:A1,link:"https://www.linkedin.com/in/ryan-spencer-attick/"},{method:"github",icon:$h,link:"https://github.com/spencerattick"},{method:"medium",icon:F1,link:"https://medium.com/@spencer.attick"},{method:"resume",icon:H1,link:J1}];function X1(){return C("div",{id:"contact",children:K1.map(e=>C("a",{href:e.link,target:"_blank",children:C(Mn,{className:"contactIcon",icon:e.icon})},e.method))})}function Z1(){const[e,t]=se.useState([]);se.useState(null);const n=o=>{let i=[];for(let s of o)(s.title.includes("Project:")||s.title.includes("🧑‍💻"))&&i.push(s);return i};se.useEffect(()=>{async function o(){try{const s=await(await fetch("http://localhost:3000/api/posts")).json();t(n(s))}catch(i){console.log(i),t(n(Gh.items))}}o()},[]);const r=o=>{let i=o.split("<"),s,l;for(const c of i)if(c.includes("cdn-images")){s=c;for(const d of s.split("="))if(d.includes("cdn-images")){l=d.split(" ")[0];break}return l.replace(/"/g,"")}},a=o=>o.replace("Project: ","");return ie("div",{id:"projects",children:[C("h2",{children:"Projects"}),C("div",{id:"posts-container",children:e.length>0?e.map(o=>{const i=r(o.content),s=a(o.title);return C("div",{className:"post",children:ie("a",{href:o.id,target:"_blank",children:[C("img",{src:i,alt:s}),C("p",{children:C("span",{children:s})})]})},o.id)}):ie("div",{children:["The server appears to be down at the moment. You can check out my projects here: ",C("a",{href:"https://medium.com/@spencer.attick",children:"Spencer's Blog"}),"."]})})]})}const ev="Spencer's Updates",tv="Recent updates from Spencer",nv="https://www.goodreads.com/user/show/104822881-spencer-attick",rv="https://www.goodreads.com/images/layout/goodreads_logo_144.jpg",av=[],ov=[{id:"Review6409737317",title:"Spencer added 'Freedom is a Constant Struggle'",description:` + Freedom is a Constant Struggle by Angela Y. Davis + Spencer gave 5 stars to Freedom is a Constant Struggle (Paperback) + by + Angela Y. Davis +
+ + + + `,link:"https://www.goodreads.com/review/show/6409737317",published:1712511806e3,created:1712511806e3,category:[],enclosures:[],media:{}},{id:"Review5836026781",title:"Spencer added 'An Autobiography'",description:` + An Autobiography by Angela Y. Davis + Spencer gave 5 stars to An Autobiography (Paperback) + by + Angela Y. Davis +
+ + + + `,link:"https://www.goodreads.com/review/show/5836026781",published:1712450674e3,created:1712450674e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7789676355",title:"Spencer wants to read 'My Grandmother: A Memoir'",description:` + My Grandmother by Fethiye Çetin + Spencer wants to read My Grandmother: A Memoir + by + Fethiye Çetin +
+ `,link:"https://www.goodreads.com/review/show/6407410317",published:1712435671e3,created:1712435671e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7789505447",title:"Spencer wants to read 'Claudette Colvin: Twice Toward Justice'",description:` + Claudette Colvin by Phillip Hoose + Spencer wants to read Claudette Colvin: Twice Toward Justice + by + Phillip Hoose +
+ `,link:"https://www.goodreads.com/review/show/6407289935",published:1712432484e3,created:1712432484e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7789503043",title:"Spencer wants to read 'The Montgomery Bus Boycott and the Women Who Started It: The Memoir of Jo Ann Gibson Robinson'",description:` + The Montgomery Bus Boycott and the Women Who Started It by Jo Ann Gibson Robinson + Spencer wants to read The Montgomery Bus Boycott and the Women Who Started It: The Memoir of Jo Ann Gibson Robinson + by + Jo Ann Gibson Robinson +
+ `,link:"https://www.goodreads.com/review/show/6407288181",published:1712432435e3,created:1712432435e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7788986390",title:"Spencer wants to read 'Normal Life by Dean Spade'",description:` + Normal Life by Dean Spade by Dean Spade + Spencer wants to read Normal Life by Dean Spade + by + Dean Spade +
+ `,link:"https://www.goodreads.com/review/show/6406918073",published:1712422109e3,created:1712422109e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7788984321",title:"Spencer wants to read 'Queer (In)Justice: The Criminalization of LGBT People in the United States'",description:` + Queer (In)Justice by Joey L. Mogul + Spencer wants to read Queer (In)Justice: The Criminalization of LGBT People in the United States + by + Joey L. Mogul +
+ `,link:"https://www.goodreads.com/review/show/6406916566",published:1712422066e3,created:1712422066e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7788982934",title:"Spencer wants to read 'Captive Genders: Trans Embodiment and the Prison Industrial Complex'",description:` + Captive Genders by Eric A. Stanley + Spencer wants to read Captive Genders: Trans Embodiment and the Prison Industrial Complex + by + Eric A. Stanley +
+ `,link:"https://www.goodreads.com/review/show/6406915533",published:1712422037e3,created:1712422037e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7775479776",title:"Spencer wants to read 'Lilith'",description:` + Lilith by Eric Rickstad + Spencer wants to read Lilith + by + Eric Rickstad +
+ `,link:"https://www.goodreads.com/review/show/6397247001",published:1712103187e3,created:1712103187e3,category:[],enclosures:[],media:{}},{id:"ReadStatus7774671812",title:"Spencer has read 'Yellowface'",description:` + Yellowface by R.F. Kuang + Spencer has read Yellowface + by + R.F. Kuang +
+ `,link:"https://www.goodreads.com/review/show/6396669101",published:1712089261e3,created:1712089261e3,category:[],enclosures:[],media:{}}],iv={title:ev,description:tv,link:nv,image:rv,category:av,items:ov};function sv(){const[e,t]=se.useState([]),[n,r]=se.useState(null);se.useEffect(()=>{a()},[]);const a=async()=>{try{const d=await(await fetch("http://localhost:3000/api/feed")).json();t(d)}catch(c){console.log(c),t(iv)}},o=c=>{const d=c.toString().slice(0,-3),m=new Date(d*1e3).toDateString().split(" ");return`${m[1]} ${m[2]} ${m[3]}`},i=c=>{let d;return c.title.includes("wants to read")?d=`added to list ${o(c.created)}`:c.title.includes("finished reading")||c.title.includes("has read")||c.title.includes("Spencer added")?d=`finished on ${o(c.created)}`:(c.title.includes("started reading")||c.title.includes("currently reading"))&&(d=`started reading on ${o(c.created)}`),d},s=c=>{const d=c.description,p=d.indexOf("books/")+5,m=d.indexOf(".jpg"),g=d.substring(p,m),y=g.split("/")[1],b=g.split("/")[2].split(".")[0];return`https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/${y}/${b}.jpg`},l=c=>(c=c.substring(c.indexOf("'")+1),c=c.slice(0,-1),c);return n?ie("div",{children:["Error: ",n.message]}):ie("div",{id:"goodreads",children:[C("h2",{children:"Currently Reading"}),C("div",{id:"goodreads-container",children:e.items?e.items.map((c,d)=>{const p=s(c),m=l(c.title),g=i(c);return C("div",{className:"book-div",children:ie("a",{href:c.link,target:"_blank",children:[C("img",{src:p,alt:m}),C("p",{children:g})]})},d)}):ie("div",{children:["It looks like something went wrong. You can see what I'm reading here: ",C("a",{href:"https://www.goodreads.com/user/show/104822881-spencer-attick",children:"Goodreads Feed"}),"."]})})]})}function lv(){const[e,t]=se.useState([]),[n,r]=se.useState(null);return se.useEffect(()=>{async function a(){try{const i=await(await fetch("http://localhost:3000/api/strava")).json();t(i)}catch(o){console.log(o),r(o)}}a()},[]),ie("div",{id:"strava",children:[C("h2",{children:"Strava"}),C("div",{children:JSON.stringify(e[0])})]})}function cv(){return ie("div",{children:[C(Sm,{}),C(Tm,{}),C(Z1,{}),C(W1,{}),C(Q1,{}),C(sv,{}),C(lv,{}),C(X1,{})]})}function uv(){return C("div",{id:"loading",children:C("h1",{children:"LOADING..."})})}Pa.createRoot(document.getElementById("loading")).render(C(io.StrictMode,{children:C(uv,{})}));setTimeout(()=>{document.getElementById("loading").remove(),Pa.createRoot(document.getElementById("root")).render(C(io.StrictMode,{children:C(cv,{})}))},1e3); diff --git a/dist/index.html b/dist/index.html index 06977fb..8ddeab3 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4,7 +4,7 @@ Spencer's Portfolio - + diff --git a/package-lock.json b/package-lock.json index 6ddb11f..281ec70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@types/node": "^18.13.0", "@vitejs/plugin-react": "^3.1.0", + "dotenv": "^16.0.3", "express": "^4.18.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1059,6 +1060,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", diff --git a/package.json b/package.json index 0e8f40a..4b9334e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@types/node": "^18.13.0", "@vitejs/plugin-react": "^3.1.0", + "dotenv": "^16.0.3", "express": "^4.18.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/server/server.js b/server/server.js index a0bce12..7f5f41f 100644 --- a/server/server.js +++ b/server/server.js @@ -2,8 +2,9 @@ const express = require('express'); const app = express(); const { parse } = require('rss-to-json'); const path = require('path'); - -// [] see about not allowing * CORS requests +const dotenv = require('dotenv'); +require('dotenv').config(); +const fs = require('fs'); // Allow cross-origin requests from Vite app.use(function(req, res, next) { @@ -35,6 +36,125 @@ app.get('/api/feed', async (req, res) => { } }); +//STRAVA ROUTE START +const isStravaTokenExpired = (currentExpirationTime) => { + console.log('IS STRAVA TOKEN EXPIRED') + const currentEpochTime = Date.now(); + if (currentExpirationTime === 'undefined') { + return `There is an error with the currentEpirationTime, it's value is: ${currentExpirationTime}.` + } + return currentEpochTime > currentExpirationTime; +} + +const generateNewToken = async () => { + console.log('Generating new token...'); + const requestOptions = { + method: 'POST', + redirect: 'follow' + }; + const requestURL = `https://www.strava.com/oauth/token?client_id=${process.env.STRAVA_CLIENT_ID}&client_secret=${process.env.STRAVA_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=${process.env.STRAVA_CACHED_REFRESH_TOKEN}`; + + try { + let response = await fetch(requestURL, requestOptions); + response = await response.json(); + if (response.message === 'Bad Request') { + console.log(response); + return; + } + console.log('NO PROBLEM GETTING NEW TOKEN') + return { + refreshToken: await response.refresh_token, + expirationTime: await response.expires_at, + accessToken: await response.access_token + } + } catch (error) { + console.log(error); + } + +} + + + +const persistNewTokenData = async (newTokenData) => { + console.log('PERSIST NEW TOKEN DATA') + // Read the .env file + const envBuffer = fs.readFileSync('.env'); + const envConfig = dotenv.parse(envBuffer); + + // // // Update the relevant key with the new value + if (newTokenData.expirationTime) { + envConfig['STRAVA_EXPIRATION_TIME'] = newTokenData.expirationTime; + } + if (newTokenData.refreshToken) { + envConfig['STRAVA_CACHED_REFRESH_TOKEN'] = newTokenData.refreshToken; + } + if (newTokenData.accessToken) { + envConfig['STRAVA_CACHED_TOKEN'] = newTokenData.accessToken; + } + + // const envText = Object.keys(envConfig).map(key => `${key}=${envConfig[key]}`).join('\n'); + // try { + // await fs.promises.writeFile('.env', envText); + // console.log('File successfully written'); + // } catch (error) { + // console.error('Error writing file:', error); + // } + +}; + +const getStravaActivityData = async () => { + console.log('GET STRAVA ACTIVITY DATA') + console.log('Requesting activity data from Strava...'); + let myHeaders = new Headers(); + myHeaders.append("Authorization", `Bearer ${process.env.STRAVA_CACHED_TOKEN}`); + + const requestOptions = { + method: 'GET', + headers: myHeaders, + redirect: 'follow' + }; + + try { + const response = await fetch("https://www.strava.com/api/v3/athlete/activities", requestOptions); + return response.json(); + + } catch (error) { + console.log('error', error); + return; + } +} + +//check to see if the expiration time has passed +const executeStravaLogic = async () => { + console.log('EXECUTE STRAVA LOGIC') + const isTokenExpired = isStravaTokenExpired(process.env.STRAVA_EXPIRATION_TIME); + console.log('TOKEN EXPIRED? ', isTokenExpired) + + if (typeof isTokenExpired === 'string') { + console.log('Please resolve the error with the current expiration time stored in the .env file.'); + return; + } else if (isTokenExpired) { + console.log('The expiration time has passed. Generating a new token...'); + //if yes - generate a new token + const newTokenData = await generateNewToken(); + if (!newTokenData.expirationTime) { + console.log('There was an error getting refresh token data.') + return; + } + //save the new token info to .env + persistNewTokenData(newTokenData); + + } + // //make request to Strava activities endpoint + const stravaActivityData = await getStravaActivityData(); + console.log(stravaActivityData[0].name); +}; + +app.get('/api/strava', executeStravaLogic); + + +//STRAVA ROUTE END + // Start the server app.listen(3000, () => { console.log('Server started on http://localhost:3000'); diff --git a/src/App.jsx b/src/App.jsx index 9d7d2d5..974548f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,6 +6,7 @@ import Blog from './Blog'; import Contact from './Contact'; import Projects from './Projects'; import GoodReads from './GoodReads'; +import Strava from './Strava'; // add movement that draws attention to important pieces of the site as you scroll @@ -18,6 +19,7 @@ export default function App() { + ) diff --git a/src/Blog.jsx b/src/Blog.jsx index 51e8cf9..11d8da8 100644 --- a/src/Blog.jsx +++ b/src/Blog.jsx @@ -57,7 +57,7 @@ const getSixPostsOrMax = data => { useEffect(() => { async function fetchBlogPosts() { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/posts`); + const response = await fetch(`http://localhost:3000/api/posts`); const data = await response.json(); setBlogPosts(getSixPostsOrMax(data)); } catch (error) { diff --git a/src/GoodReads.jsx b/src/GoodReads.jsx index 50e57d1..8cc482e 100644 --- a/src/GoodReads.jsx +++ b/src/GoodReads.jsx @@ -22,7 +22,7 @@ export default function GoodReads() { const fetchGoodReadsFeed = async () => { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/feed`); + const response = await fetch(`http://localhost:3000/api/feed`); const data = await response.json(); setGoodReadsFeed(data); } catch (error) { diff --git a/src/Projects.jsx b/src/Projects.jsx index f919609..3a57763 100644 --- a/src/Projects.jsx +++ b/src/Projects.jsx @@ -25,7 +25,7 @@ export default function Projects() { useEffect(() => { async function fetchBlogPosts() { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/posts`); + const response = await fetch(`http://localhost:3000/api/posts`); const data = await response.json(); setProjectPosts(getProjectPosts(data)); } catch (error) { diff --git a/src/Strava.jsx b/src/Strava.jsx index f95670b..29b88b6 100644 --- a/src/Strava.jsx +++ b/src/Strava.jsx @@ -1,47 +1,32 @@ -// https://developers.strava.com/docs/getting-started/#account - -// get an API key - - - - -// FOLLOWING STEPS HERE: https://developers.strava.com/docs/getting-started/#oauth (this vid is pretty good: https://www.youtube.com/watch?v=sgscChKfGyg) - - -// 1. http://localhost/exchange_token?state=&code=62b8194da84fd2f4455851e52b26f49a1ca9991e&scope=read,read_all - -// 2. curl --location --request POST 'https://www.strava.com/oauth/token?client_id=105494&client_secret=aeca4b4832d195c467d23e1c465781eed04be76b&code=62b8194da84fd2f4455851e52b26f49a1ca9991e&grant_type=authorization_code' \ -// --header 'Authorization: Basic SXI5RjJySjRYWUhNRTNuYjJvSDhPYU5zaklNVlFmdzY6' \ -// --data '' - -// { -// "token_type": "Bearer", -// "expires_at": 1681258716, -// "expires_in": 21600, -// "refresh_token": "cc5a66ffbe22e42dac7aa175097485bffa03b123", -// "access_token": "b6ddf246cde306a6520efaf8c8686ce0d95cdfab", -// "athlete": { -// "id": 8470431, -// "username": "sattick", -// "resource_state": 2, -// "firstname": "Spencer", -// "lastname": "Attick", -// "bio": null, -// "city": "Oakland", -// "state": "California", -// "country": "United States", -// "sex": "M", -// "premium": false, -// "summit": false, -// "created_at": "2015-04-01T00:46:54Z", -// "updated_at": "2023-03-23T00:01:05Z", -// "badge_type_id": 0, -// "weight": 0.0, -// "profile_medium": "https://graph.facebook.com/1226490129/picture?height=256&width=256", -// "profile": "https://graph.facebook.com/1226490129/picture?height=256&width=256", -// "friend": null, -// "follower": null -// } -// } - -3. \ No newline at end of file +import React, { useEffect, useState } from 'react'; +// import staticFeed from '../assets/staticGoodreadsFeed.json'; + +export default function Strava() { + + const [stravaData, setStravaData] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchStravaData() { + try { + const response = await fetch(`http://localhost:3000/api/strava`); + const data = await response.json(); + setStravaData(data); + } catch (error) { + console.log(error); + setError(error); + } + } + fetchStravaData(); + }, []); + + + return ( +
+

Strava

+
+ {JSON.stringify(stravaData[0])} +
+
+ ) +} \ No newline at end of file diff --git a/src/StravaAPIReference.jsx b/src/StravaAPIReference.jsx new file mode 100644 index 0000000..87bd12a --- /dev/null +++ b/src/StravaAPIReference.jsx @@ -0,0 +1,182 @@ +const dotenv = require('dotenv'); +require('dotenv').config(); +const fs = require('fs'); + + +//NEED TO MOVE THIS TO SERVER +//UNDEFINED VALUES ARE BEING STORED TO .ENV FILE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +const checkIfStravaTokenIsExpired = (expirationTime) => { + if (!expirationTime) { + return true; + } + console.log('Checking if token is still valid...'); + const currentEpochTime = Math.floor(new Date().getTime() / 1000); + return currentEpochTime > expirationTime; +} + +const getNewStravaToken = async (cachedRefreshToken) => { + const requestOptions = { + method: 'POST', + redirect: 'follow' + }; + + try { + let response = await fetch(`https://www.strava.com/oauth/token?client_id=${process.env.STRAVA_CLIENT_ID}&client_secret=${process.env.STRAVA_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=${cachedRefreshToken}`, requestOptions); + response = await response.json(); + console.log('STRAVAAADATAAA: ', response) + if (!response.ok) { + console.log(`Response status is ${response.status}`); + return 'error'; + } + + return { + refreshToken: await response.refresh_token, + expirationTime: await response.expires_at, + accessToken: await response.access_token + } + + } catch (error) { + console.log(error); + } + +} + + + +const getDataFromStrava = async () => { + console.log('Requesting activity data from Strava...'); + let myHeaders = new Headers(); + myHeaders.append("Authorization", `Bearer ${process.env.STRAVA_CACHED_TOKEN}`); + + console.log('TOKENN', process.env.STRAVA_CACHED_TOKEN) + console.log(`HEADERSS ${JSON.stringify(myHeaders)}`); + + + const requestOptions = { + method: 'GET', + headers: myHeaders, + redirect: 'follow' + }; + + console.log(`Request Options: ${JSON.stringify(requestOptions)}`); + + + try { + const response = await fetch("https://www.strava.com/api/v3/athlete/activities", requestOptions); + console.log(`Request URL: ${response.url}`); + return response.json(); + + } catch (error) { + console.log('error', error) + } +}; + +const manageRefreshToken = async () => { + const isTokenExpired = checkIfStravaTokenIsExpired(process.env.STRAVA_EXPIRATION_TIME); + if (isTokenExpired === 'error') { + console.log('There was an error getting the refresh token!') + return; + } else if (isTokenExpired === true) { + console.log("The token is still valid!"); + } else { + // If the data is not in the cache, get it from the server and cache it + console.log("The token has expired! Fetching a new one!"); + const newStravaData = await getNewStravaToken(process.env.STRAVA_CACHED_REFRESH_TOKEN); + + console.log('NEW STRAVA DATA: ', await newStravaData) + + //update the .env values + //update the .env values + const envBuffer = fs.readFileSync('.env'); + const envConfig = dotenv.parse(envBuffer); + envConfig.STRAVA_EXPIRATION_TIME = newStravaData.expirationTime; + envConfig.STRAVA_CACHED_REFRESH_TOKEN = newStravaData.cachedToken; + envConfig.STRAVA_CACHED_TOKEN = newStravaData.cachedRefreshToken; + const envText = Object.keys(envConfig).map((key) => `${key}=${envConfig[key]}`).join('\n'); + await fs.promises.writeFile('.env', envText); + + } + getDataFromStrava() + .then(data => console.log(data)); +}; + + // Use the getData function to fetch data from a URL + manageRefreshToken(); + + + + +// https://developers.strava.com/docs/getting-started/#account + +// const { get } = require("http") + +// get an API key + +//CODE WILL NEED TO CHECK TO SEE IF TOKEN IS VALID + //IF SO PROCEDE + //IF NOT WILL NEED TO GENERATE A NEW ONE + + //USE THIS TO CACHE: https://medium.com/@spencer.attick/a-guide-to-immediately-invoked-function-expressions-iife-17f6e29a07ba + + //should be able to refresh like this: https://developers.strava.com/docs/authentication/#refreshingexpiredaccesstokens:~:text=curl%20%2DX%20POST%20https%3A//www.strava.com/api/v3/oauth/token%20%5C%0A%20%20%2Dd%20client_id%3DReplaceWithClientID%20%5C%0A%20%20%2Dd%20client_secret%3DReplaceWithClientSecret%20%5C%0A%20%20%2Dd%20grant_type%3Drefresh_token%20%5C%0A%20%20%2Dd%20refresh_token%3DReplaceWithRefreshToken + + +// FOLLOWING STEPS HERE: https://developers.strava.com/docs/getting-started/#oauth (this vid is pretty good: https://www.youtube.com/watch?v=sgscChKfGyg) + + +//AUTHORIZE IN THE UI TO GET THE CODE +// 1. http://www.strava.com/oauth/authorize?client_id=105494&response_type=code&redirect_uri=http://localhost/exchange_token&approval_prompt=force&scope=activity:read_all + +// http://localhost/exchange_token?state=&code=1c49f418910a609b0097ae5ce0016c9b8141e8cd&scope=read,activity:read_all + + +//MAKE A REQUEST WITH POSTMAN TO GET THE ACCESS TOKEN +// 2. curl --location 'https://www.strava.com/oauth/token?client_id=105494&client_secret=aeca4b4832d195c467d23e1c465781eed04be76b&code=1c49f418910a609b0097ae5ce0016c9b8141e8cd&grant_type=authorization_code' \ +// --header 'Content-Type: application/json' \ + +// { +// "token_type": "Bearer", +// "expires_at": 1681350948, +// "expires_in": 21600, +// "refresh_token": "25bd1cf38e37ee07e53d446f10ef812daee4d9bc", +// "access_token": "87b070c5b099c8b17398cc3f6884e2645ccf222a", +// "athlete": { +// "id": 8470431, +// "username": "sattick", +// "resource_state": 2, +// "firstname": "Spencer", +// "lastname": "Attick", +// "bio": null, +// "city": "Oakland", +// "state": "California", +// "country": "United States", +// "sex": null, +// "premium": false, +// "summit": false, +// "created_at": "2015-04-01T00:46:54Z", +// "updated_at": "2023-04-11T18:24:23Z", +// "badge_type_id": 0, +// "weight": 0.0, +// "profile_medium": "https://graph.facebook.com/1226490129/picture?height=256&width=256", +// "profile": "https://graph.facebook.com/1226490129/picture?height=256&width=256", +// "friend": null, +// "follower": null +// } +// } + +// USE THE ACCESS TOKEN AS THE BEARER TO MAKE A REQUEST (CANNOT HAVE A BODY FOR GET) +// 3. curl --location 'https://www.strava.com/api/v3/athlete/activities' \ +// --header 'Authorization: Bearer 87b070c5b099c8b17398cc3f6884e2645ccf222a' + +// THERE IS DATA RETURNED - IT WAS LONG AND TOO ANNOYING TO KEEP HERE + +// 4. REFRESH TOKEN +// curl --location --request POST 'https://www.strava.com/oauth/token?client_id=105494&client_secret=aeca4b4832d195c467d23e1c465781eed04be76b&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=25bd1cf38e37ee07e53d446f10ef812daee4d9bc' + +// { +// "token_type": "Bearer", +// "access_token": "6e6e9dbf5c63983cf2cf018278a3dc1b3a0f129c", +// "expires_at": 1681430228, +// "expires_in": 21600, +// "refresh_token": "25bd1cf38e37ee07e53d446f10ef812daee4d9bc" +// } \ No newline at end of file diff --git a/src/StravaServerLogic.jsx b/src/StravaServerLogic.jsx new file mode 100644 index 0000000..4353b88 --- /dev/null +++ b/src/StravaServerLogic.jsx @@ -0,0 +1,153 @@ +const dotenv = require('dotenv'); +require('dotenv').config(); +const fs = require('fs'); + +const isStravaTokenExpired = (currentExpirationTime) => { + const currentEpochTime = Date.now(); + if (currentExpirationTime === 'undefined') { + return `There is an error with the currentEpirationTime, it's value is: ${currentExpirationTime}.` + } + return currentEpochTime > currentExpirationTime; +} + +const generateNewToken = async () => { + const requestOptions = { + method: 'POST', + redirect: 'follow' + }; + const requestURL = `https://www.strava.com/oauth/token?client_id=${process.env.STRAVA_CLIENT_ID}&client_secret=${process.env.STRAVA_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=ReplaceWithRefreshToken&refresh_token=${process.env.STRAVA_CACHED_REFRESH_TOKEN}`; + + try { + let response = await fetch(requestURL, requestOptions); + response = await response.json(); + if (response.message === 'Bad Request') { + return 'Something is wrong with the request to generate a new Strava token.'; + } + return { + refreshToken: await response.refresh_token, + expirationTime: await response.expires_at, + accessToken: await response.access_token + } + } catch (error) { + console.log(error); + } + +} + +const persistNewTokenData = async (newTokenData) => { + const envBuffer = fs.readFileSync('.env'); + const envConfig = dotenv.parse(envBuffer); + envConfig.STRAVA_EXPIRATION_TIME = newTokenData.expirationTime; + envConfig.STRAVA_CACHED_REFRESH_TOKEN = newTokenData.refreshToken; + envConfig.STRAVA_CACHED_TOKEN = newTokenData.accessToken; + const envText = Object.keys(envConfig).map((key) => `${key}=${envConfig[key]}`).join('\n'); + await fs.promises.writeFile('.env', envText); +} + +const getStravaActivityData = async () => { + console.log('Requesting activity data from Strava...'); + let myHeaders = new Headers(); + myHeaders.append("Authorization", `Bearer ${process.env.STRAVA_CACHED_TOKEN}`); + + const requestOptions = { + method: 'GET', + headers: myHeaders, + redirect: 'follow' + }; + + try { + const response = await fetch("https://www.strava.com/api/v3/athlete/activities", requestOptions); + return response.json(); + + } catch (error) { + console.log('error', error) + } +} + +//check to see if the expiration time has passed +const executeStravaLogic = (async () => { + const isTokenExpired = isStravaTokenExpired(process.env.STRAVA_EXPIRATION_TIME); + + if (typeof isTokenExpired === 'string') { + console.log('Please resolve the error with the current expiration time stored in the .env file.'); + return; + } else if (isTokenExpired) { + console.log('The expiration time has passed. Generating a new token...'); + //if yes - generate a new token + const newTokenData = await generateNewToken(); + if (!newTokenData.expirationTime) { + console.log('There was an error getting refresh token data.') + return; + } + //save the new token info to .env + persistNewTokenData(newTokenData); + + } + //make request to Strava activities endpoint + const stravaActivityData = await getStravaActivityData(); + console.log(stravaActivityData[0]); +})(); + +//IMPORTANT FILEDS RETURNED FROM ACTIVITIES API: +//name +//distance +//moving_time +//type +//start_date +//average_speed +//max_speed + + +// { +// resource_state: 2, +// athlete: { id: 8470431, resource_state: 1 }, +// name: 'Afternoon Run', +// distance: 4030, +// moving_time: 1716, +// elapsed_time: 1811, +// total_elevation_gain: 4.4, +// type: 'Run', +// sport_type: 'Run', +// workout_type: 0, +// id: 8890903152, +// start_date: '2023-04-14T19:01:08Z', +// start_date_local: '2023-04-14T15:01:08Z', +// timezone: '(GMT-05:00) America/New_York', +// utc_offset: -14400, +// location_city: null, +// location_state: null, +// location_country: 'United States', +// achievement_count: 0, +// kudos_count: 0, +// comment_count: 0, +// athlete_count: 1, +// photo_count: 0, +// map: { +// id: 'a8890903152', +// summary_polyline: 'mo{hFpc|sMSk@i@gA{AeE_BsD_@o@Mi@q@_BGGOIQYWSM[]g@CBDBEBD@@KB?[cAE[Ym@Uy@Ys@@_@CW?m@QsAA]@[LYGI?EHCJWFE\\CVIrAi@nB}@RET?FBHFHPTt@VhApA|DZnAf@|AzAzDVZDLNLVr@VZFLJHd@x@b@|@rBhHd@pAD`@?TGNMNi@\\mBp@iCt@]@WKMMKSgC_HSi@wAsCc@eAIc@SYOc@MMKMGEK[KGGKW]UKi@mAEW]gAOm@Qg@C_@OYMu@I[A[Ga@EaBH]?WJOTEBEBWDC\\DjBu@r@_@~@]VAJDNNHLx@pCHRHh@x@lDZ`AVl@h@~A^`A^l@d@j@LVV\\Rd@h@t@b@`AbBzFXv@V`AHp@AJIT', +// resource_state: 2 +// }, +// trainer: false, +// commute: false, +// manual: false, +// private: false, +// visibility: 'everyone', +// flagged: false, +// gear_id: null, +// start_latlng: [ 38.31864684820175, -76.82715729810297 ], +// end_latlng: [ 38.31877291202545, -76.82764202356339 ], +// average_speed: 2.348, +// max_speed: 3.948, +// has_heartrate: false, +// heartrate_opt_out: false, +// display_hide_heartrate_option: false, +// elev_high: 6.9, +// elev_low: 4.6, +// upload_id: 9540098951, +// upload_id_str: '9540098951', +// external_id: '9E17EEF1-BC5D-44FC-B2FD-7C5FC7EB319E-activity.fit', +// from_accepted_tag: false, +// pr_count: 0, +// total_photo_count: 0, +// has_kudoed: false +// } \ No newline at end of file diff --git a/src/test.js b/src/test.js new file mode 100644 index 0000000..e799dbe --- /dev/null +++ b/src/test.js @@ -0,0 +1,32 @@ +var generate = function(numRows) { + const pascalsArr = []; + let rowCounter = 0; + + //i is the row you're on + //rowCounter is the num of that row + + for (let i = 0; i <= numRows; i++) { + let rowToPush = []; + if (i === 0) rowToPush.push(1); + //previous row's i and i - 1 added together + while (rowCounter >= 0 && i > 0) { + let previousRow = pascalsArr[i - 1]; + if (previousRow[rowCounter - 1] === undefined) { + rowToPush.push(1); + } else if (previousRow[rowCounter] === undefined) { + rowToPush.push(1); + } else { + rowToPush.push(previousRow[rowCounter] + previousRow[rowCounter - 1]) + } + + rowCounter--; + } + + pascalsArr.push(rowToPush); + rowCounter = i + 1; + } + return pascalsArr; +}; + +console.log(generate(5)); +// Output: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] \ No newline at end of file