Merge branch 'main' into uwu
# Conflicts: # astro.config.ts # package-lock.json # package.json # src/styles/post-body.scss
@@ -3,6 +3,7 @@
|
||||
title: "Fun with Types",
|
||||
description: "Making hilarious things with no emitted code.",
|
||||
published: "2023-01-05T20:35:30Z",
|
||||
edited: "2023-06-25T00:35:10Z",
|
||||
authors: ["maisydino"],
|
||||
tags: ["typescript"],
|
||||
attached: [],
|
||||
@@ -18,7 +19,7 @@ I've had a _lot_ of fun over the years writing TypeScript, and I've delved deep
|
||||
Here's an interesting one I came accross when browsing through some of the issues on the TypeScript repo.
|
||||
|
||||
```ts
|
||||
function foo(input: "foo" | "bar" | string) {
|
||||
function foo(input: "foo" | "bar" | (string & {})) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
BIN
content/blog/intro-to-hash-tables/ht_art_img01.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
content/blog/intro-to-hash-tables/ht_art_img02.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
content/blog/intro-to-hash-tables/ht_art_img03.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
111
content/blog/intro-to-hash-tables/index.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
{
|
||||
title: "Introduction to Hash Tables",
|
||||
description: "A high-level overview of how hash tables work behind the scenes.",
|
||||
published: '2023-07-03',
|
||||
authors: ["richarddprasad"],
|
||||
tags: ["hash tables", "data structures"],
|
||||
license: 'cc-by-nc-nd-4'
|
||||
}
|
||||
---
|
||||
|
||||
A hash table - also known as a dictionary or hash map - is a type of data structure used to store large amounts of data. Data consists of key-value pairs that are inserted into a table-like structure.
|
||||
|
||||

|
||||
|
||||
In our example, we have keys which are integers (numbers) and values that are strings. Hash tables are made up of two parts:
|
||||
|
||||
1. An array of addresses, each of which is referred to as a **slot** (or **bucket**). This part of the hash table is called the **prime area**;
|
||||
2. A collision resolution area. Our example uses linked lists to handle collisions. Collisions and linked lists will be covered shortly.
|
||||
|
||||
In general, a hash table attempts to stuff objects into a fixed number of slots. This is achieved by transforming an object's key using a hash function. Objects which produce the same hash key all share the same slot.
|
||||
|
||||
We could have just as easily used an ordinary array to store our data. This raises the question of why bother with hash tables and their seemingly convoluted way of storing data? The answer lies in the limitations of arrays.
|
||||
|
||||
# Starting Simple: Arrays
|
||||
|
||||
An array stores objects of the same type within a single, contiguous block of memory. An array has a fixed size, and increasing the size of an array is not possible. This is because there is no way to guarantee there is enough free memory immediately after the array in which to expand it. In the example below, we could not expand our array to add an object with value "G" because the block of memory after the array is in use.
|
||||
|
||||

|
||||
|
||||
Decreasing an array's size introduces its own set of problems. The only way to resize an array is to make a new array, copy the contents of the old one into the new one, then delete the old one. For small arrays, this is usually not an issue. However, for large amounts of data, arrays become too expensive to work with.
|
||||
|
||||
If we know our array is not going to change over the course of our application, then using an array is probably the best option. However, if we need to add or remove objects frequently, arrays are a poor choice. An array relies on index position to refer to each object it contains. Index positioning only works if the following holds true:
|
||||
|
||||
1. All elements in the array are of the same type and thus the same size;
|
||||
2. All elements in the array are laid out in memory one after the other in a single block;
|
||||
3. The length of the array is known ahead of time.
|
||||
|
||||
An array's indexing system is great - it enables constant access times for any element in the array - but it comes at the cost of flexibility. What we need is a way to free ourselves from the foregoing requirements to create an infinitely-flexible list of data.
|
||||
|
||||
# Going to the Next Level: Linked Lists
|
||||
|
||||
A linked list is yet another data structure for storing data, but it is not limited in having to place its contents within a single block of memory. Our objects can live anywhere within memory and are simply linked together in a chain-like structure. To achieve this, our objects must be wrapped into wrapper objects known as **nodes**:
|
||||
|
||||
```typescript
|
||||
// TypeScript example using generic parameter T
|
||||
// Fields are public to avoid getter/setter overhead
|
||||
class Node<T> {
|
||||
public item: T;
|
||||
public next: Node<T> | null;
|
||||
|
||||
constructor(item: T) {
|
||||
this.item = item;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A node contains a reference to another node called `next`. We can link as many nodes together as we desire - memory permitting, of course - freeing ourselves from the limitations of arrays. Adding or removing nodes from a linked list are trivial operations compared to resizing a large array.
|
||||
|
||||

|
||||
|
||||
Unfortunately, linked lists are not without their downsides as well. There is no way to directly access a particular element in a linked list. If we had a linked list with 5,000,000 elements and wanted the last one, we would have to traverse the entire list before getting to 5,000,000th element. Wouldn't it be nicer if we could somehow split our giant linked list into a number of smaller ones?
|
||||
|
||||
# The Best of Both Worlds: Hash Tables
|
||||
|
||||
Our version of a hash table is nothing more than an array of linked lists. It should be noted this is not the only way to implement a hash table, but it is certainly the most flexible in terms of size.
|
||||
|
||||
Each array item is a reference to a linked list and is known as a slot. The idea behind a hash table is to divide our large array or list into a number of smaller linked lists, essentially changing our one-dimensional array/list into a two-dimensional (hash) table. So how do we determine which slot to toss an object into? This is where the hash function comes into play.
|
||||
|
||||
## The Hash Function
|
||||
|
||||
A hash function transforms a provided key into a "hash key" which corresponds with the index of a slot; this "hash key" is called a **home address**. The process of transforming a key into a home address is known as **key-to-address mapping**.
|
||||
|
||||
In our example, our hash table has 5 slots, with home addresses of 0-4. Thus our hash function must transform our objects' keys into values from 0-4. There are many ways to generate a hash function. We are going to take the easy and lazy route: Given that our objects' keys are simple numbers, we can use the modulus operator to reduce our keys to the desired range.
|
||||
|
||||
```
|
||||
homeAddress = objectKey mod lengthOfHashTable
|
||||
```
|
||||
|
||||
Our hash table has 5 slots, so `lengthOfHashTable` is 5. An object with key 1007, when fed into the hash function, produces a home address of 2:
|
||||
|
||||
```
|
||||
1007 mod 5 = 2
|
||||
```
|
||||
|
||||
The technique we are using is called the **modulo-division technique**. There are a number of other techniques as well, e.g., midsquare method, digit-extraction method, etc.
|
||||
|
||||
Ideally, if we had a 5,000,000-item linked list, we would want our five-slot hash table to divide those elements into five, 1,000,000-item linked lists. Smaller lists are faster to traverse, and the cost of computing the home address is trivial.
|
||||
|
||||
## Collisions and Collision Chains
|
||||
|
||||
When more than one object produces the same home address, it is said a **collision** has occurred. For example, *Cat* and *Dog* both generate the same home address of 2 and have thus collided. We resolve collisions by using a **collision chain**. In our example, we are using linked lists for this purpose. Once the home address is computed, we ask the linked list at the computed home address to use the original key to find the desired object.
|
||||
|
||||
As mentioned, this is not the only way to implement a hash table. Just as there are other techniques for creating a hash function, there are also other techniques for handling collisions (open addressing, bucket hashing, etc.).
|
||||
|
||||
Using a linked list incurs a space complexity cost: The node wrappers around our objects, as well as the `next` reference links, cause our code to use more memory. A linked list collision chain is ill-suited for small memory environments such as microcontroller devices.
|
||||
|
||||
A poorly-designed hash function is also a cause for concern: What if our hash function simply ends up throwing everything into only one or a few slots, leaving the rest empty? This phenomenon is known as **clustering** and we want to avoid it at all costs - if all our objects ended up in a single slot, we would just end up with another linked list!
|
||||
|
||||
# Conclusion
|
||||
|
||||
Hash tables can offer substantial performance benefits when storing and searching through large amounts of data. However, that is not their only use. Hash tables are used to facilitate the creation of databases and virtual machines. They go hand-in-hand with various algorithms, such graph algorithms that enable technologies such as Google Maps. Hash tables can also be used to create dynamic objects with properties that can be added or removed at runtime (think JavaScript objects).
|
||||
|
||||
Given how useful hash tables are, practically every programming language these days includes a standard implementation ready to go, saving you the trouble of having to roll your own.
|
||||
|
||||
---
|
||||
|
||||
### References
|
||||
|
||||
Gilberg, Richard and Behrouz Forouzan. *Data Structures: A Pseudocode Approach with C, 2nd Edition*. Course Technology, 2005.
|
||||
BIN
content/blog/js-classes-without-keyword/class_compat.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
364
content/blog/js-classes-without-keyword/index.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
{
|
||||
title: "Using JavaScript classes without the `class` keyword",
|
||||
description: "Classes are a core feature of JavaScript - but they weren't always that way. How did earlier JS devs write classes? Let's learn how together.",
|
||||
published: '2023-06-29T21:52:59.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['javascript'],
|
||||
attached: [],
|
||||
license: 'cc-by-4'
|
||||
}
|
||||
---
|
||||
|
||||
Classes in JavaScript are both powerful and weird. While they allow us to create named objects with similarly purposed methods and properties, they're often misunderstood because of nuanced in the language itself.
|
||||
|
||||
But did you know that prior to 2015, JavaScript didn't even have a `class` keyword as part of the language?
|
||||
|
||||
Despite this, many programs at the time used classic Object Oriented Programming (OOP) methodologies such as using a class, extending it, and even adding static methods.
|
||||
|
||||
> But without a `class` method, how did they even make classes?
|
||||
|
||||
A good question! Let's answer that and, along the way, look at:
|
||||
|
||||
- How to create a "class" without the `class` keyword
|
||||
- How to "extend" a "class"
|
||||
- How to add static methods to our "class"
|
||||
|
||||
# Create public fields with the `contructor`
|
||||
|
||||
Let's look at a modern JavaScript class:
|
||||
|
||||
```javascript
|
||||
class User {
|
||||
name = "Corbin",
|
||||
username = "crutchcorn",
|
||||
sayCatchphrase() {
|
||||
console.log("It depends");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a fairly basic class that has two properties (`name` and `username`) as well as a `sayCatchphrase` method.
|
||||
|
||||
However, despite [the `class` keyword being added in 2015 with ES6](https://262.ecma-international.org/6.0/#sec-class-definitions), [public fields like this weren't added until ECMAScript 2020](https://github.com/tc39/proposal-class-fields#implementations):
|
||||
|
||||
[](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields)
|
||||
|
||||
> So then how did classes get properties in years after 2015 but before 2020?
|
||||
|
||||
The answer? The `constructor` method:
|
||||
|
||||
```javascript {1-4}
|
||||
class User {
|
||||
constructor() {
|
||||
this.name = "Corbin",
|
||||
this.username = "crutchcorn",
|
||||
}
|
||||
|
||||
sayCatchphrase() {
|
||||
console.log("It depends");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In fact, using this `constructor` method, we can even add the method as well:
|
||||
|
||||
```javascript {4-6}
|
||||
class User {
|
||||
constructor() {
|
||||
this.name = "Corbin",
|
||||
this.username = "crutchcorn",
|
||||
this.sayCatchphrase = function() {
|
||||
console.log("It depends");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> An interesting fact, for sure - but it doesn't answer the question of how to make a class.
|
||||
|
||||
Don't worry, we're getting there!
|
||||
|
||||
# Create a class without the `class` keyword
|
||||
|
||||
Before we answer the question of "how to make a class in JavaScript without the `class` keyword", let's take a step back and look at what a `class` is actually doing...
|
||||
|
||||
After all, a class like `User` above might create an object like so:
|
||||
|
||||
```javascript
|
||||
const userObject = {
|
||||
name: "Corbin",
|
||||
username: "crutchcorn",
|
||||
sayCatchphrase: function() {
|
||||
console.log("It depends");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Knowing this, we might think that the best way to make a class without the keyword is to return an object from a function:
|
||||
|
||||
```javascript
|
||||
function User() {
|
||||
return {
|
||||
name: "Corbin",
|
||||
username: "crutchcorn",
|
||||
sayCatchphrase: function() {
|
||||
console.log("It depends");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And sure enough, if we run this code using:
|
||||
|
||||
```javascript
|
||||
const user = new User();
|
||||
user.sayCatchphrase(); // "It depends"
|
||||
```
|
||||
|
||||
It will run as-expected. However, it won't solve all cases. EG:
|
||||
|
||||
```javascript
|
||||
new User() instanceof User; // false
|
||||
```
|
||||
|
||||
Instead, what if we just converted the aforementioned class' `constructor` body to a function?:
|
||||
|
||||
```javascript
|
||||
function User() {
|
||||
this.name = "Corbin";
|
||||
this.username = "crutchcorn";
|
||||
this.sayCatchphrase = function() {
|
||||
console.log("It depends");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, not only do we have the method working, but `instanceof` works as well:
|
||||
|
||||
```javascript
|
||||
const user = new User();
|
||||
user.sayCatchphrase(); // "It depends"
|
||||
new User() instanceof User; // true
|
||||
```
|
||||
|
||||
## Prototype Manipulation
|
||||
|
||||
> But surely changing from a class to a function doesn't allow you to change the prototype in the same way?
|
||||
|
||||
Actually, it does! That's how this whole thing works!
|
||||
|
||||
Consider the following code:
|
||||
|
||||
```javascript
|
||||
function User() {
|
||||
this.name = "Corbin";
|
||||
this.username = "crutchcorn";
|
||||
}
|
||||
|
||||
User.prototype.sayCatchphrase = function() {
|
||||
console.log("It depends");
|
||||
}
|
||||
```
|
||||
|
||||
This is the same way of adding a method as the `this.sayCatchphrase` method as before, but is done by changing the prototype.
|
||||
|
||||
We can test this code still works by running:
|
||||
|
||||
```javascript
|
||||
const user = new User();
|
||||
user.sayCatchphrase(); // "It depends"
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Create an extended class using the `super` method
|
||||
|
||||
Before we talk about function-based class extension, we need to talk about pre-ES2020 class creation once again.
|
||||
|
||||
See, when we convert the following code to use a `contructor`:
|
||||
|
||||
```javascript
|
||||
class Person {
|
||||
personality = "quirky";
|
||||
}
|
||||
|
||||
class Corbin extends Person {
|
||||
name = "Corbin";
|
||||
}
|
||||
```
|
||||
|
||||
Like so:
|
||||
|
||||
```javascript
|
||||
class Person {
|
||||
constructor() {
|
||||
this.personality = "quirky";
|
||||
}
|
||||
}
|
||||
|
||||
class Corbin extends Person {
|
||||
constructor() {
|
||||
this.name = "Corbin";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And try to initialize it:
|
||||
|
||||
```javascript
|
||||
const corn = new Corbin()
|
||||
```
|
||||
|
||||
We get the following error:
|
||||
|
||||
```
|
||||
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
|
||||
at new Corbin (<anonymous>:9:6)
|
||||
```
|
||||
|
||||
This is because we're not using the `super()` method to tell our extended class to utilize the parent's class' methods.
|
||||
|
||||
To fix this, we'll add that method to the extended class' `constructor`:
|
||||
|
||||
```javascript {8}
|
||||
class Person {
|
||||
constructor() {
|
||||
this.personality = "quirky";
|
||||
}
|
||||
}
|
||||
|
||||
class Corbin extends Person {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "Corbin";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now our `Corbin` constructor work work as-intended:
|
||||
|
||||
```javascript
|
||||
const corn = new Corbin();
|
||||
console.log(corn.name); // "Corbin";
|
||||
console.log(corn.personality); // "quirky";
|
||||
```
|
||||
|
||||
# Extend a functional class using `Object.create`
|
||||
|
||||
Let's now convert our `Person` and `Corbin` classes to use functions instead of the `class` keyword.
|
||||
|
||||
The person class is easy enough:
|
||||
|
||||
```javascript
|
||||
function Person() {
|
||||
this.personality = "quirky";
|
||||
}
|
||||
```
|
||||
|
||||
And we _could_ use [the `call` method to bind `Person`'s `this` to `Corbin`](/posts/javascript-bind-usage#bind), like so:
|
||||
|
||||
```javascript
|
||||
function Corbin() {
|
||||
Person.call(this);
|
||||
this.name = "Corbin";
|
||||
}
|
||||
```
|
||||
|
||||
And it appears to work at first:
|
||||
|
||||
```javascript
|
||||
const corn = new Corbin();
|
||||
console.log(corn.name); // "Corbin";
|
||||
console.log(corn.personality); // "quirky";
|
||||
```
|
||||
|
||||
But now, once again, if we call `instanceof` it doesn't support the base class:
|
||||
|
||||
```javascript
|
||||
new Corbin() instanceof Corbin; // true
|
||||
new Corbin() instanceof Person; // false
|
||||
```
|
||||
|
||||
To fix this, we need to tell JavaScript to use the `prototype ` of `Person` and combine it with the prototype of `Corbin`, like so:
|
||||
|
||||
```javascript
|
||||
function Person() {
|
||||
}
|
||||
|
||||
Person.prototype.personality = "quirky";
|
||||
|
||||
function Corbin() {
|
||||
}
|
||||
|
||||
Corbin.prototype = Object.create(Person.prototype);
|
||||
Corbin.prototype.name = "Corbin";
|
||||
|
||||
const corn = new Corbin();
|
||||
corn.personality // "quirky"
|
||||
corn.name // "Corbin"
|
||||
|
||||
const pers = new Person();
|
||||
pers.personality // "quirky"
|
||||
pers.name // undefined
|
||||
```
|
||||
|
||||
> Notice how we're using [`Object.create`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) to create a base object from the other prototype
|
||||
|
||||
# Static Methods
|
||||
|
||||
Let's wrap up this article by talking about how to add static methods to a functional class.
|
||||
|
||||
As a refresher, this is what a static method looks like on a ES2020 class:
|
||||
|
||||
```javascript
|
||||
class User {
|
||||
name = "Corbin",
|
||||
username = "crutchcorn",
|
||||
static sayCatchphrase() {
|
||||
console.log("It depends");
|
||||
}
|
||||
}
|
||||
|
||||
User.sayCatchphrase(); // "It depends"
|
||||
User.name // undefined
|
||||
|
||||
const corn = new User();
|
||||
corn.name; // "Corbin"
|
||||
```
|
||||
|
||||
This can be added by providing a key to the function's name outside of the function body:
|
||||
|
||||
```javascript
|
||||
function User() {
|
||||
this.name = "Corbin",
|
||||
this.username = "crutchcorn",
|
||||
}
|
||||
|
||||
User.sayCatchphrase() {
|
||||
console.log("It depends");
|
||||
}
|
||||
|
||||
|
||||
User.sayCatchphrase(); // "It depends"
|
||||
User.name // undefined
|
||||
|
||||
const corn = new User();
|
||||
corn.name; // "Corbin"
|
||||
```
|
||||
|
||||
# Conclusion
|
||||
|
||||
This has been an interesting look into how to use JavaScript classes without the `class` keyword.
|
||||
|
||||
Hopefully, this has helped dispel some misunderstandings about how classes work in JavaScript or maybe just given historical context for why some code is written how it is.
|
||||
|
||||
Like learning JavaScript's fundamentals?
|
||||
|
||||
[Check out my article that explains how to use the `.bind` keyword in JavaScript.](/posts/javascript-bind-usage#bind)
|
||||
|
||||
Read it and want more?
|
||||
|
||||
[Check out my book that teaches the introduction of React, Angular, and Vue all at once; "The Framework Field Guide".](https://framework.guide)
|
||||
|
||||
Until next time!
|
||||
BIN
content/blog/port-nextjs-to-astro/homepage.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
628
content/blog/port-nextjs-to-astro/index.md
Normal file
@@ -0,0 +1,628 @@
|
||||
---
|
||||
{
|
||||
title: 'Porting a Next.js Site to Astro Step-by-Step',
|
||||
description: "Let's port a site from Next.js to Astro, expanding on the official migration guide.",
|
||||
published: '2023-06-29T22:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['nextjs', 'react', 'astro'],
|
||||
attached: [],
|
||||
license: 'cc-by-nc-sa-4'
|
||||
}
|
||||
---
|
||||
|
||||
[Astro is a WebDev meta-framework](https://astro.build) that allows you to build highly performant websites, that, out-of-the-box compile down to 0kb of JavaScript in your bundle.
|
||||
|
||||
If your site is a marketing site, or if performance is a substantial concern for your app, it may make sense to migrate from Next.js to Astro - as we [at Unicorn Utterances](https://unicorn-utterances.com) once did.
|
||||
|
||||
While Astro provides a good guide of [how to migrate from Next.js to Astro](https://docs.astro.build/en/guides/migrate-to-astro/from-nextjs/) (written by yours truly!), I felt it would be helpful to see an expanded step-by-step guide on how to migrate a [Pokédex](https://www.pokemon.com/us/pokedex) application from Next.js to Astro.
|
||||
|
||||

|
||||
|
||||
> For the full codebase of the Pokédex application, [check the original repo here](https://github.com/crutchcorn/nextjs-pokemon-small-app).
|
||||
|
||||
|
||||
|
||||
Let's start the migration by changing our layout file to an Astro layout file.
|
||||
|
||||
## Next base layout to Astro
|
||||
|
||||
To start migrating a Next.js layout file from Next.js to Astro, you'll:
|
||||
|
||||
1. Identify the return().
|
||||
|
||||
```jsx {5-17}
|
||||
// _document.js
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html>
|
||||
<Head lang="en">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</Head>
|
||||
<body>
|
||||
<div className="screen">
|
||||
<div className='screen-contents'>
|
||||
<Main />
|
||||
</div>
|
||||
</div>
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
2. Create `Layout.astro` and add this `return` value, [converted to Astro syntax](#reference-convert-nextjs-syntax-to-astro).
|
||||
|
||||
Note that:
|
||||
|
||||
- `<Html>` becomes `<html>`
|
||||
- `<Head>` becomes `<head>`
|
||||
- `<Main />` becomes `<slot />`
|
||||
- `className` becomes `class`
|
||||
- We do not need `<NextScript>`
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/layouts/Layout.astro
|
||||
---
|
||||
<html>
|
||||
<head lang="en">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
<div class='screen-contents'>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
3. Import the CSS (found in `_app.js`)
|
||||
|
||||
In addition to the `_document` file, the Next.js application has a `_app.js` file that imports global styling via a CSS import:
|
||||
|
||||
```jsx {2}
|
||||
// pages/_app.js
|
||||
import '../styles/index.css'
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
```
|
||||
|
||||
This CSS import can be moved to the Astro Layout component:
|
||||
|
||||
```astro {1-4}
|
||||
---
|
||||
// src/layouts/Layout.astro
|
||||
import '../styles/index.css'
|
||||
---
|
||||
|
||||
<html>
|
||||
<head lang="en">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
<div class='screen-contents'>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Convert a Next.js `getStaticProps` Page to Astro
|
||||
|
||||
Next up, let's migrate [Next.js' `getStaticProps` method](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props) to use Astro's data fetching.
|
||||
|
||||
We'll start with a Next.js page that lists the first 151 Pokémon using [the REST PokéAPI](https://pokeapi.co/):
|
||||
|
||||
```jsx
|
||||
// pages/index.js
|
||||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
import styles from '../styles/poke-list.module.css';
|
||||
|
||||
export default function Home({ pokemons }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Pokedex: Generation 1</title>
|
||||
</Head>
|
||||
<ul className={`plain-list ${styles.pokeList}`}>
|
||||
{pokemons.map((pokemon) => (
|
||||
<li className={styles.pokemonListItem} key={pokemon.name}>
|
||||
<Link className={styles.pokemonContainer} as={`/pokemon/${pokemon.name}`} href="/pokemon/[name]">
|
||||
<p className={styles.pokemonId}>No. {pokemon.id}</p>
|
||||
<img className={styles.pokemonImage} src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`} alt={`${pokemon.name} picture`}></img>
|
||||
<h2 className={styles.pokemonName}>{pokemon.name}</h2>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
|
||||
const resJson = await res.json();
|
||||
const pokemons = resJson.results.map(pokemon => {
|
||||
const name = pokemon.name;
|
||||
// https://pokeapi.co/api/v2/pokemon/1/
|
||||
const url = pokemon.url;
|
||||
const id = url.split("/")[url.split("/").length - 2];
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
id
|
||||
}
|
||||
});
|
||||
return {
|
||||
props: {
|
||||
pokemons,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Move Next Page Templating to Astro
|
||||
|
||||
To start migrating this page to Astro, start with the returned JSX and place it within an `.astro` file:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/index.astro
|
||||
import styles from '../styles/poke-list.module.css';
|
||||
---
|
||||
|
||||
<head>
|
||||
<title>Pokedex: Generation 1</title>
|
||||
</head>
|
||||
<ul class={`plain-list ${styles.pokeList}`}>
|
||||
{pokemons.map((pokemon) => (
|
||||
<li class={styles.pokemonListItem} key={pokemon.name}>
|
||||
<a class={styles.pokemonContainer} href={`/pokemon/${pokemon.name}`}>
|
||||
<p class={styles.pokemonId}>No. {pokemon.id}</p>
|
||||
<img class={styles.pokemonImage} src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`} alt={`${pokemon.name} picture`}></img>
|
||||
<h2 class={styles.pokemonName}>{pokemon.name}</h2>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
During the migration to Astro templating, this example also:
|
||||
|
||||
- Imported styles move to the code fence
|
||||
- Removed the `<>` container fragment, as it is not needed in Astro's template.
|
||||
- Changed `className` to a more standard `class` attribute.
|
||||
- Migrated the Next `<Link>` component to an `<a>` HTML element.
|
||||
|
||||
Now move the `<head>` into your existing `layout.astro` file. To do this, we can:
|
||||
|
||||
1. Pass the `title` property to the `layout.astro` file via `Astro.props`
|
||||
2. Import the layout file in `/src/pages/index.astro`
|
||||
3. Wrap the Astro page's template in the Layout component
|
||||
|
||||
```astro {5,11}
|
||||
---
|
||||
// src/layouts/Layout.astro
|
||||
import '../styles/index.css'
|
||||
|
||||
const {title} = Astro.props;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head lang="en">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
<div class='screen-contents'>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```astro {4,7,19}
|
||||
---
|
||||
// src/pages/index.astro
|
||||
import styles from '../styles/poke-list.module.css';
|
||||
import Layout from '../layouts/layout.astro';
|
||||
---
|
||||
|
||||
<Layout title="Pokedex: Generation 1">
|
||||
<ul class={`plain-list ${styles.pokeList}`}>
|
||||
{pokemons.map((pokemon) => (
|
||||
<li class={styles.pokemonListItem} key={pokemon.name}>
|
||||
<a class={styles.pokemonContainer} href={`/pokemon/${pokemon.name}`}>
|
||||
<p class={styles.pokemonId}>No. {pokemon.id}</p>
|
||||
<img class={styles.pokemonImage} src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`} alt={`${pokemon.name} picture`}></img>
|
||||
<h2 class={styles.pokemonName}>{pokemon.name}</h2>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
### Move Next Page Logic Requests to Astro
|
||||
|
||||
This is the `getStaticProps` method from the Next.js page:
|
||||
|
||||
```jsx
|
||||
export const getStaticProps = async () => {
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
|
||||
const resJson = await res.json();
|
||||
const pokemons = resJson.results.map(pokemon => {
|
||||
const name = pokemon.name;
|
||||
// https://pokeapi.co/api/v2/pokemon/1/
|
||||
const url = pokemon.url;
|
||||
const id = url.split("/")[url.split("/").length - 2];
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
id
|
||||
}
|
||||
});
|
||||
return {
|
||||
props: {
|
||||
pokemons,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This then passes the `props` into the `Home` component that's been defined:
|
||||
|
||||
```jsx
|
||||
export default function Home({ pokemons }) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In Astro, this process is different. Instead of using a dedicated `getStaticProps` function, move the props logic into the code fence of our Astro page:
|
||||
|
||||
```astro {5-17}
|
||||
---
|
||||
// src/pages/index.astro
|
||||
import styles from '../styles/poke-list.module.css';
|
||||
import Layout from '../layouts/layout.astro';
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151");
|
||||
const resJson = await res.json();
|
||||
const pokemons = resJson.results.map(pokemon => {
|
||||
const name = pokemon.name;
|
||||
// https://pokeapi.co/api/v2/pokemon/1/
|
||||
const url = pokemon.url;
|
||||
const id = url.split("/")[url.split("/").length - 2];
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
id
|
||||
}
|
||||
});
|
||||
---
|
||||
|
||||
<Layout title="Pokedex: Generation 1">
|
||||
<ul class={`plain-list ${styles.pokeList}`}>
|
||||
{pokemons.map((pokemon) => (
|
||||
<li class={styles.pokemonListItem} key={pokemon.name}>
|
||||
<a class={styles.pokemonContainer} href={`/pokemon/${pokemon.name}`}>
|
||||
<p class={styles.pokemonId}>No. {pokemon.id}</p>
|
||||
<img class={styles.pokemonImage} src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`} alt={`${pokemon.name} picture`}></img>
|
||||
<h2 class={styles.pokemonName}>{pokemon.name}</h2>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
You should now have a fully working Pokédex entries screen.
|
||||
|
||||
## Convert a Next.js `getStaticPaths` Page to Astro
|
||||
|
||||
This is a Next.js dynamic page that generates a detail screen for each of the first 151 Pokémon using [the REST PokéAPI](https://pokeapi.co/).
|
||||
|
||||
```jsx
|
||||
// pages/pokemon/[name].js
|
||||
import { useRouter } from 'next/router';
|
||||
import Head from 'next/head'
|
||||
import styles from '../../styles/pokemon-entry.module.css';
|
||||
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export default function Pokemon({ pokemon }) {
|
||||
const router = useRouter();
|
||||
const title = `Pokedex: ${pokemon.name}`;
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<button onClick={() => router.back()} className={styles.backBtn} aria-label="Go back"></button>
|
||||
<img className={styles.pokeImage} src={pokemon.image} alt={`${pokemon.name} picture`} />
|
||||
<div className={styles.infoContainer}>
|
||||
<h1 className={styles.header}>No. {pokemon.id}: {pokemon.name}</h1>
|
||||
<table className={styles.pokeInfo}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Types</th>
|
||||
<td>{pokemon.types}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Height</th>
|
||||
<td>{pokemon.height}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weight</th>
|
||||
<td>{pokemon.weight}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p className={styles.flavor}>{pokemon.flavorText}</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
|
||||
const resJson = await res.json();
|
||||
const pokemons = resJson.results;
|
||||
|
||||
return {
|
||||
paths: pokemons.map(({ name }) => ({
|
||||
params: { name },
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticProps = async (context) => {
|
||||
const { name } = context.params
|
||||
const [pokemon, species] = await Promise.all([
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${name}`).then(res => res.json()),
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon-species/${name}`).then(res => res.json())
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
pokemon: {
|
||||
id: pokemon.id,
|
||||
image: pokemon.sprites.front_default,
|
||||
name: capitalize(pokemon.name),
|
||||
height: pokemon.height,
|
||||
weight: pokemon.weight,
|
||||
flavorText: species.flavor_text_entries[0].flavor_text,
|
||||
types: pokemon.types.map(({ type }) => type.name).join(', ')
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Move Next Page Templating to Astro
|
||||
|
||||
To start migrating this page to Astro, start with the returned JSX and place it within an `.astro` file:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/pokemon/[name].astro
|
||||
import styles from '../../styles/pokemon-entry.module.css';
|
||||
---
|
||||
|
||||
<Layout title={`Pokedex: ${pokemon.name}`}>
|
||||
<button onclick="history.go(-1)" class={styles.backBtn} aria-label="Go back"></button>
|
||||
<img class={styles.pokeImage} src={pokemon.image} alt={`${pokemon.name} picture`} />
|
||||
<div class={styles.infoContainer}>
|
||||
<h1 class={styles.header}>No. {pokemon.id}: {pokemon.name}</h1>
|
||||
<table class={styles.pokeInfo}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Types</th>
|
||||
<td>{pokemon.types}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Height</th>
|
||||
<td>{pokemon.height}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weight</th>
|
||||
<td>{pokemon.weight}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class={styles.flavor}>{pokemon.flavorText}</p>
|
||||
</div>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
Like before:
|
||||
|
||||
- Imported styles are moved to the code fence.
|
||||
- `className` becomes `class`.
|
||||
- `<Head>` contents are moved into `<Layout>`.
|
||||
- `{pokemon.id}` values are interpolated the same as before.
|
||||
|
||||
However, in addition, now:
|
||||
|
||||
- [HTML's standard `onclick`](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties) function is used to call [the browser's `history.go` function](https://developer.mozilla.org/en-US/docs/Web/API/History/go) to navigate back.
|
||||
|
||||
### Move Next `getStaticPaths` to Astro
|
||||
|
||||
Astro supports a function called `getStaticPaths` to generate dynamic paths, similar to Next.
|
||||
|
||||
Given a Next page:
|
||||
|
||||
```jsx
|
||||
// pages/pokemon/[name].js
|
||||
export const getStaticPaths = async () => {
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
|
||||
const resJson = await res.json();
|
||||
const pokemons = resJson.results;
|
||||
|
||||
return {
|
||||
paths: pokemons.map(({ name }) => ({
|
||||
params: { name },
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Migrate the `getStaticPaths` method to Astro by removing the `paths` route prefix and returning an array:
|
||||
|
||||
```astro {10-12}
|
||||
---
|
||||
// src/pages/pokemon/[name].astro
|
||||
import styles from '../../styles/pokemon-entry.module.css';
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
|
||||
const resJson = await res.json();
|
||||
const pokemons = resJson.results;
|
||||
|
||||
return pokemons.map(({ name }) => ({
|
||||
params: { name },
|
||||
}))
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title={`Pokedex: ${pokemon.name}`}>
|
||||
<button onclick="history.go(-1)" class={styles.backBtn} aria-label="Go back"></button>
|
||||
<img class={styles.pokeImage} src={pokemon.image} alt={`${pokemon.name} picture`} />
|
||||
<div class={styles.infoContainer}>
|
||||
<h1 class={styles.header}>No. {pokemon.id}: {pokemon.name}</h1>
|
||||
<table class={styles.pokeInfo}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Types</th>
|
||||
<td>{pokemon.types}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Height</th>
|
||||
<td>{pokemon.height}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weight</th>
|
||||
<td>{pokemon.weight}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class={styles.flavor}>{pokemon.flavorText}</p>
|
||||
</div>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
Then, similar to the previous page, migrate the `getStaticProps` method to non-function-wrapped code in the Astro page's code fence.
|
||||
|
||||
Given the Next page logic:
|
||||
```jsx
|
||||
// pages/pokemon/[name].js
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export const getStaticProps = async (context) => {
|
||||
const { name } = context.params
|
||||
const [pokemon, species] = await Promise.all([
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${name}`).then(res => res.json()),
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon-species/${name}`).then(res => res.json())
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
pokemon: {
|
||||
id: pokemon.id,
|
||||
image: pokemon.sprites.front_default,
|
||||
name: capitalize(pokemon.name),
|
||||
height: pokemon.height,
|
||||
weight: pokemon.weight,
|
||||
flavorText: species.flavor_text_entries[0].flavor_text,
|
||||
types: pokemon.types.map(({ type }) => type.name).join(', ')
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Migrate this to the Astro page's code fence:
|
||||
|
||||
:::tip
|
||||
Use `Astro.props` to access the `params` returned from the `getStaticPaths` function
|
||||
:::
|
||||
|
||||
```astro {15-33}
|
||||
---
|
||||
// src/pages/pokemon/[name].astro
|
||||
import styles from '../../styles/pokemon-entry.module.css';
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
|
||||
const resJson = await res.json();
|
||||
const pokemons = resJson.results;
|
||||
|
||||
return pokemons.map(({ name }) => ({
|
||||
params: { name },
|
||||
}))
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
const { name } = Astro.props;
|
||||
const [pokemonData, species] = await Promise.all([
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${name}`).then(res => res.json()),
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon-species/${name}`).then(res => res.json())
|
||||
])
|
||||
|
||||
const pokemon = {
|
||||
id: pokemonData.id,
|
||||
image: pokemonData.sprites.front_default,
|
||||
name: capitalize(pokemonData.name),
|
||||
height: pokemonData.height,
|
||||
weight: pokemonData.weight,
|
||||
flavorText: species.flavor_text_entries[0].flavor_text,
|
||||
types: pokemonData.types.map(({ type }) => type.name).join(', ')
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={`Pokedex: ${pokemon.name}`}>
|
||||
<button onclick="history.go(-1)" class={styles.backBtn} aria-label="Go back"></button>
|
||||
<img class={styles.pokeImage} src={pokemon.image} alt={`${pokemon.name} picture`} />
|
||||
<div class={styles.infoContainer}>
|
||||
<h1 class={styles.header}>No. {pokemon.id}: {pokemon.name}</h1>
|
||||
<table class={styles.pokeInfo}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Types</th>
|
||||
<td>{pokemon.types}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Height</th>
|
||||
<td>{pokemon.height}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weight</th>
|
||||
<td>{pokemon.weight}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class={styles.flavor}>{pokemon.flavorText}</p>
|
||||
</div>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
You have now fully migrated a Pokédex application from Next to Astro.
|
||||
|
||||
1382
content/blog/setup-a-react-native-monorepo/index.md
Normal file
BIN
content/blog/setup-a-react-native-monorepo/rn_monorepo.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
@@ -3,7 +3,7 @@
|
||||
title: "What's An Algorithm?",
|
||||
description: "A quick introduction into what algorithms are, what they're made of and why they're an important part of understanding how programming languages work",
|
||||
published: '2022-08-26T18:00:00.000Z',
|
||||
authors: ['qarnax'],
|
||||
authors: ['xenophorium'],
|
||||
tags: ['computer science'],
|
||||
attached: [],
|
||||
license: 'cc-by-nc-sa-4',
|
||||
|
||||
|
Before Width: | Height: | Size: 116 KiB |
@@ -344,17 +344,17 @@
|
||||
"roles": ["author"]
|
||||
},
|
||||
{
|
||||
"id": "qarnax",
|
||||
"name": "Qarnax",
|
||||
"id": "xenophorium",
|
||||
"name": "Xeno",
|
||||
"firstName": "",
|
||||
"lastName": "",
|
||||
"description": "I'm a frontend developer and indie game enthusiast 👾 \n I enjoy learning new things and building my own stuff 🔧 and I love helping people get into coding 😊",
|
||||
"socials": {
|
||||
"twitch": "qarnax_",
|
||||
"twitter": "qarnax",
|
||||
"github": "qarnax801"
|
||||
"twitch": "xenophorium",
|
||||
"github": "xenophorium",
|
||||
"website": "https://card.xenophorium.dev"
|
||||
},
|
||||
"profileImg": "./qarnax.jpg",
|
||||
"profileImg": "./xeno.jpg",
|
||||
"color": "",
|
||||
"roles": ["developer", "author", "community", "translator"]
|
||||
},
|
||||
@@ -537,5 +537,19 @@
|
||||
"profileImg": "./sarahgerrard.jpg",
|
||||
"color": "#49A078",
|
||||
"roles": ["author"]
|
||||
},
|
||||
{
|
||||
"id": "richarddprasad",
|
||||
"name": "Richard Prasad",
|
||||
"firstName": "Richard",
|
||||
"lastName": "Prasad",
|
||||
"description": "Paralegal transitioning into software engineering",
|
||||
"socials": {
|
||||
"github": "richarddprasad"
|
||||
},
|
||||
"pronouns": "he",
|
||||
"profileImg": "./hello.png",
|
||||
"color": "#1AB5E5",
|
||||
"roles": ["author"]
|
||||
}
|
||||
]
|
||||
|
||||
BIN
content/data/xeno.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
12
package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/preact": "^3.2.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/json5": "^2.2.0",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
@@ -53,6 +54,7 @@
|
||||
"image-size": "^1.0.2",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-watch-typeahead": "^2.2.2",
|
||||
"json5": "^2.2.3",
|
||||
"junk": "^4.0.1",
|
||||
"lint-staged": "^13.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -3455,6 +3457,16 @@
|
||||
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==",
|
||||
"deprecated": "This is a stub types definition. json5 provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"json5": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/make-fetch-happen": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.0.tgz",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/preact": "^3.2.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/json5": "^2.2.0",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
@@ -79,6 +80,7 @@
|
||||
"image-size": "^1.0.2",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-watch-typeahead": "^2.2.2",
|
||||
"json5": "^2.2.3",
|
||||
"junk": "^4.0.1",
|
||||
"lint-staged": "^13.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
||||
110
src/styles/markdown/file-list.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
.docs-file-tree {
|
||||
--x-space: 1.75em;
|
||||
--y-pad: 0.2em;
|
||||
|
||||
display: block;
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem 0.5rem;
|
||||
background-color: var(--shiki-color-background);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.docs-file-tree .directory > details,
|
||||
.docs-file-tree .directory > details:hover,
|
||||
.docs-file-tree .directory > details[open] {
|
||||
margin-bottom: unset;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
padding-inline-start: var(--x-space);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.docs-file-tree .directory > details > summary,
|
||||
.docs-file-tree .directory > details[open] > summary {
|
||||
background: unset;
|
||||
border: 0;
|
||||
padding: var(--y-pad) 0.3em;
|
||||
font-weight: normal;
|
||||
color: var(--black);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.docs-file-tree .directory > details > summary::marker,
|
||||
.docs-file-tree .directory > details > summary::-webkit-details-marker {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.docs-file-tree .directory > details > summary:hover .tree-icon,
|
||||
.docs-file-tree .directory > details > summary:hover {
|
||||
color: var(--lightPrimary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.docs-file-tree ul:is(ul),
|
||||
.docs-file-tree .directory > details ul:is(ul) {
|
||||
margin: 0;
|
||||
margin-inline-start: calc(1em - 1px);
|
||||
border-inline-start: 2px solid var(--minImpactBlack);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.docs-file-tree > ul:is(ul) {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.docs-file-tree li:is(li) {
|
||||
margin: 0;
|
||||
padding: var(--y-pad) 0;
|
||||
}
|
||||
|
||||
.docs-file-tree .file {
|
||||
margin-inline-start: var(--x-space);
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.docs-file-tree .tree-entry {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
max-width: calc(100% - 1.25em);
|
||||
}
|
||||
|
||||
@media (min-width: 30em) {
|
||||
.docs-file-tree .tree-entry {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-file-tree .tree-entry > :first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.docs-file-tree .empty {
|
||||
color: var(--lowImpactBlack);
|
||||
padding-inline-start: 0.5em;
|
||||
}
|
||||
|
||||
.docs-file-tree .comment {
|
||||
color: var(--lowImpactBlack);
|
||||
padding-inline-start: 2em;
|
||||
max-width: 30em;
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
.docs-file-tree .highlight {
|
||||
display: inline-block;
|
||||
border-radius: 0.3em;
|
||||
padding-inline-end: 0.5em;
|
||||
outline: 1px solid var(--lightPrimary);
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
fill: currentColor;
|
||||
vertical-align: middle;
|
||||
margin: -0.75em 0.2em;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
@@ -154,3 +154,4 @@
|
||||
@import "src/styles/markdown/lists";
|
||||
@import "src/styles/markdown/table";
|
||||
@import "src/styles/markdown/tooltips";
|
||||
@import "src/styles/markdown/file-list";
|
||||
|
||||
756
src/utils/markdown/file-tree/file-tree-icons.ts
Normal file
192
src/utils/markdown/file-tree/rehype-file-tree.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* This was taken from Astro docs:
|
||||
* https://github.com/withastro/docs/blob/83e4e7946933b468f857c76f8d4f9861e37d7059/src/components/internal/rehype-file-tree.ts
|
||||
*
|
||||
* But modified such that:
|
||||
* - It works with HTML comments
|
||||
* - You can attach metadata to files and folders
|
||||
* - It is required to use inline code blocks for files and folders names
|
||||
*/
|
||||
/**
|
||||
* Usage:
|
||||
* <!-- filetree:start -->
|
||||
* - `somedir/`
|
||||
* - `otherdir/`
|
||||
* - `index.ts`
|
||||
* - `src/{open: false}`
|
||||
* - `index.html`
|
||||
* - `index.css`
|
||||
* <!-- filetree:end -->
|
||||
*/
|
||||
import { fromHtml } from "hast-util-from-html";
|
||||
import { toString } from "hast-util-to-string";
|
||||
import { h } from "hastscript";
|
||||
import type { Element, HChild } from "hastscript/lib/core";
|
||||
import { CONTINUE, SKIP, visit } from "unist-util-visit";
|
||||
import { getIcon } from "./file-tree-icons";
|
||||
import replaceAllBetween from "unist-util-replace-all-between";
|
||||
import { Node } from "unist";
|
||||
import { Root } from "hast";
|
||||
import JSON5 from "json5";
|
||||
|
||||
/** Make a text node with the pass string as its contents. */
|
||||
const Text = (value = ""): { type: "text"; value: string } => ({
|
||||
type: "text",
|
||||
value,
|
||||
});
|
||||
|
||||
/** Convert an HTML string containing an SVG into a HAST element node. */
|
||||
const makeSVGIcon = (svgString: string) => {
|
||||
const root = fromHtml(svgString, { fragment: true });
|
||||
const svg = root.children[0] as Element;
|
||||
svg.properties = {
|
||||
...svg.properties,
|
||||
width: 16,
|
||||
height: 16,
|
||||
class: "tree-icon",
|
||||
"aria-hidden": "true",
|
||||
};
|
||||
return svg;
|
||||
};
|
||||
|
||||
const FileIcon = (filename: string) => {
|
||||
const { svg } = getIcon(filename);
|
||||
return makeSVGIcon(svg);
|
||||
};
|
||||
|
||||
const FolderIcon = makeSVGIcon(
|
||||
'<svg viewBox="-5 -5 26 26"><path d="M1.8 1A1.8 1.8 0 0 0 0 2.8v10.4c0 1 .8 1.8 1.8 1.8h12.4a1.8 1.8 0 0 0 1.8-1.8V4.8A1.8 1.8 0 0 0 14.2 3H7.5a.3.3 0 0 1-.2-.1l-.9-1.2A2 2 0 0 0 5 1H1.7z"/></svg>'
|
||||
);
|
||||
|
||||
export const rehypeFileTree = () => {
|
||||
return (tree) => {
|
||||
function replaceFiletreeNodes(nodes: Node[]) {
|
||||
const root = { type: "root", children: nodes } as Root;
|
||||
visit(root, "element", (node) => {
|
||||
// Strip nodes that only contain newlines
|
||||
node.children = node.children.filter(
|
||||
(child) =>
|
||||
child.type === "comment" ||
|
||||
child.type !== "text" ||
|
||||
!/^\n+$/.test(child.value)
|
||||
);
|
||||
|
||||
if (node.tagName !== "li") return CONTINUE;
|
||||
|
||||
// Ensure node has properties so we can assign classes later.
|
||||
if (!node.properties) node.properties = {};
|
||||
|
||||
const [firstChild, ...otherChildren] = node.children;
|
||||
|
||||
/**
|
||||
* If a file or folder has an object at the end, assume it's a metadata object
|
||||
* that we want to associate with the file or folder.
|
||||
*
|
||||
* @eg: `folder/ {open: false}`
|
||||
*/
|
||||
let metadata: { open?: boolean } = {};
|
||||
|
||||
visit({ type: "root", children: [firstChild] }, "text", (node) => {
|
||||
const match = node.value.match(/(.*)\s*({.*})\s*$/);
|
||||
if (match) {
|
||||
node.value = match[1];
|
||||
metadata = JSON5.parse(match[2]);
|
||||
}
|
||||
});
|
||||
|
||||
const comment: HChild[] = [];
|
||||
if (firstChild.type === "text") {
|
||||
const [filename, ...fragments] = firstChild.value.split(" ");
|
||||
firstChild.value = filename;
|
||||
comment.push(fragments.join(" "));
|
||||
}
|
||||
const subTreeIndex = otherChildren.findIndex(
|
||||
(child) => child.type === "element" && child.tagName === "ul"
|
||||
);
|
||||
const commentNodes =
|
||||
subTreeIndex > -1
|
||||
? otherChildren.slice(0, subTreeIndex)
|
||||
: [...otherChildren];
|
||||
otherChildren.splice(
|
||||
0,
|
||||
subTreeIndex > -1 ? subTreeIndex : otherChildren.length
|
||||
);
|
||||
comment.push(...commentNodes);
|
||||
|
||||
const firstChildTextContent = toString(firstChild);
|
||||
|
||||
// Decide a node is a directory if it ends in a `/` or contains another list.
|
||||
const isDirectory =
|
||||
/\/\s*$/.test(firstChildTextContent) ||
|
||||
otherChildren.some(
|
||||
(child) => child.type === "element" && child.tagName === "ul"
|
||||
);
|
||||
const isPlaceholder = /^\s*(\.{3}|…)\s*$/.test(firstChildTextContent);
|
||||
const isHighlighted =
|
||||
firstChild.type === "element" && firstChild.tagName === "strong";
|
||||
const hasContents = otherChildren.length > 0;
|
||||
|
||||
const fileExtension = isDirectory
|
||||
? "dir"
|
||||
: firstChildTextContent.trim().split(".").pop() || "";
|
||||
|
||||
const icon = h(
|
||||
"span",
|
||||
isDirectory ? FolderIcon : FileIcon(firstChildTextContent)
|
||||
);
|
||||
if (!icon.properties) icon.properties = {};
|
||||
if (isDirectory) {
|
||||
icon.properties["aria-label"] = "Directory";
|
||||
}
|
||||
|
||||
node.properties.class = isDirectory ? "directory" : "file";
|
||||
if (isPlaceholder) node.properties.class += " empty";
|
||||
node.properties["data-filetype"] = fileExtension;
|
||||
|
||||
const treeEntry = h(
|
||||
"span",
|
||||
{ class: "tree-entry" },
|
||||
h("span", { class: isHighlighted ? "highlight" : "" }, [
|
||||
isPlaceholder ? null : icon,
|
||||
firstChild,
|
||||
]),
|
||||
Text(comment.length > 0 ? " " : ""),
|
||||
comment.length > 0
|
||||
? h("span", { class: "comment" }, ...comment)
|
||||
: Text()
|
||||
);
|
||||
|
||||
if (isDirectory) {
|
||||
node.children = [
|
||||
h("details", { open: metadata.open ?? hasContents }, [
|
||||
h("summary", treeEntry),
|
||||
...(hasContents ? otherChildren : [h("ul", h("li", "…"))]),
|
||||
]),
|
||||
];
|
||||
// Continue down the tree.
|
||||
return CONTINUE;
|
||||
}
|
||||
|
||||
node.children = [treeEntry, ...otherChildren];
|
||||
|
||||
// Files can’t contain further files or directories, so skip iterating children.
|
||||
return SKIP;
|
||||
});
|
||||
|
||||
return [h("div", { class: "docs-file-tree" }, root.children)];
|
||||
}
|
||||
|
||||
replaceAllBetween(
|
||||
tree,
|
||||
{ type: "raw", value: "<!-- filetree:start -->" } as never,
|
||||
{ type: "raw", value: "<!-- filetree:end -->" } as never,
|
||||
replaceFiletreeNodes
|
||||
);
|
||||
replaceAllBetween(
|
||||
tree,
|
||||
{ type: "comment", value: " filetree:start " } as never,
|
||||
{ type: "comment", value: " filetree:end " } as never,
|
||||
replaceFiletreeNodes
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "./rehype-absolute-paths";
|
||||
import { rehypeFixTwoSlashXHTML } from "./rehype-fix-twoslash-xhtml";
|
||||
import { rehypeHeaderText } from "./rehype-header-text";
|
||||
import { rehypeFileTree } from "./file-tree/rehype-file-tree";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type RehypePlugin = Plugin<any[]> | [Plugin<any[]>, any];
|
||||
@@ -73,6 +74,6 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
|
||||
rehypeWordCount,
|
||||
]
|
||||
: []),
|
||||
...(config.format === "html" ? [rehypeHeaderText] : []),
|
||||
...(config.format === "html" ? [rehypeFileTree, rehypeHeaderText] : []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -162,9 +162,9 @@ This guide offers a brilliant overview of concepts that can apply to almost any
|
||||
`.trim()}
|
||||
/>
|
||||
<QuoteCard
|
||||
personName={`Qarnax`}
|
||||
personLink="https://www.qarnax.dev/"
|
||||
avatarSrc={import("../../../../../public/content/data/qarnax.jpg")}
|
||||
personName={`Xeno`}
|
||||
personLink="https://www.xenophorium.dev/"
|
||||
avatarSrc={import("../../../../../public/content/data/xeno.jpg")}
|
||||
personTitle={`Frontend Developer`}
|
||||
quote={`
|
||||
The amount of improvement I gained within a period of 3 months through interactions with Corbin (both about the book and outside of it) was way more than what I had learned in my two professional years as a frontend developer because, at some point, I was kind of stuck in a cycle of mundane tasks and was questioning why I did frontend in the first place; so, in a way, I have him to thank for helping me find joy in coding again.
|
||||
|
||||