---
# System prepended metadata

title: Lab 1 | Nodes
tags: [lab]

---

---
title: Lab 1 | Nodes
tags: lab
---
#### [⬅️ Return to course website](https://cs1951v-2022.pages.dev/#labs-box)


# <span style="font-size: 50px;">**Lab 1: Nodes**</span>
:::info
**Released: September 15th** 
**Due: September 21st 11:59pm ET**
To get checked off for this lab, meet with a TA at your lab section or TA hours
:::

<!-- INFO BOXES 
:::success GREEN
This is for any step by step instructions that students should follow. 
:::

:::info BLUE
This is for important assignment information throughout the assignment:
**Released: September 8th, 6:00pm ET** 
**Due: September 15th, 11:59pm ET** 
:::

:::warning YELLOW
Use this info box for disclaimers.
:::

:::danger 
Use this info box for important points that students should not miss!
:::

:::spoiler Dropdown list
Use this 
:::
 -->

<!-- TYPESCRIPT CODE BLOCKS  
```typescript
const list = [10, 20];
console.log(list.map(x => (x * x)))
```
-->

 
<!-- HOW TO CHANGE COLOR IN MARKDOWN
<span style="background:aliceblue">some text with a **lightblue** background</span>

<span style="color:red">some **red** text</span> 
-->

<!-- These are a list of shortcuts available. -->
*[HTML]: Hyper Text Markup Language
*[W3C]:  World Wide Web Consortium
*[NPM]: Node Package Manager
*[IDE]: Integrated Development Environment
*[MERN]: MongoDB, Express, React, NodeJS
*[Yarn]: Yet Another Resource Negotiator


# **Introduction**

You're overwhelmed by the vast collection of donuts available. You’re lost, and unfortunately there is no way to traverse the universe of donuts. You’re unable to manage the mess of hyperdonuts and find yourself lost in hyperspace! Looks like you’ll need to come up with a means of orienting yourself to find the most scrumptious donut. 

<div style="text-align:center; margin-bottom: 20px"><img style="border-radius:5px; width:400px" src="https://media.makeameme.org/created/what-are-you-c8eaa34925.jpg" /></div>


In the `Nodes` unit, you will create a node file system - a standard way of managing and navigating between Hypertext nodes. **At the end, you will have built a full stack application that will allow users to create, read, delete, and move hypermedia nodes.** 

:::info 
**Lab Objective:** The objective for this lab is to explore the MERN stack from frontend to backend. You will go through the entire process of connecting to a MongoDB database, creating and inserting a node into the Mongo database, using Express middleware to make calls between the frontend and backend, and lastly visualizing the node's content and other relevant information as a React component in your web application. 
:::


# **Checklist**

- [ ] Accept the `Nodes` GitHub Classroom assignment
- [ ] Read `Background Information`
- [ ] Database
    - [ ] **MongoDB setup** - accessing the shared collection of nodes inside of the course MongoDB
- [ ] Backend
    - [ ] **Create Node** - Implement backend functionality to support `createNode` by making changes in the following files
        - [ ] `NodeRouter.ts`
        - [ ] `NodeGateway.ts`
        - [ ] `NodeCollectionConnection.ts`
- [ ] Frontend
    - [ ] CreateNode in Frontend `NodeGateway.ts`
    - [ ] Create React components to render the content of `text` and `image` nodes.
- [ ] Get checked off by a TA during your lab section or at TA hours before the due date

# Accepting the Assignment

For Lab 1 and Assignment 1, you will be using the same repository.  Accept the assignment [**here**](https://classroom.github.com/a/YHiUj-WQ). Once you’ve accepted your GitHub repo, clone it locally, and you should be ready to go!

:::spoiler Helpful Git CLI commands
Cloning your repo locally:
- `git clone <link>`

Add all changes in the working directory to the staging area:
- `git add -A`

Create new commit from the files in the staging area:
- `git commit -m '<commit message>'`
- Make sure to use an informative commit message in case you need to revert your changes!

Update the remote branch with all local commits:
- `git push`
:::

# Demo

You can find a demo of the MyHypermedia application [**here**](https://basic-hypermedia.web.app/). 

Note: There may be some unexpected behavior in this application as it is not set up to handle concurrency properly; this means that the application might behave weirdly if multiple users are interacting with the demo at the same time. 

# Background Information

These are some important concepts that you will come across during the lab. We understand that they can be confusing at first, so please ask if anything doesn't make sense once you use it!

## Architecture
This diagram shows the hierarchy of our full stack application. The interactions between each of the components of our system may seem complex at first, so refer to this diagram when implementing different parts of the assignment.

<div style="text-align:center; margin-bottom: 20px"><img style="border-radius:5px; width:300px" src="https://i.imgur.com/MknBs3S.png" /></div>

The frontend code uses the React framework, written in TypeScript and models the interactive part of our web application that the user will see. The React application is compiled into plain HTML, CSS and Javascript and is then hosted by a server at a specific endpoint (i.e. "http/my-web-app.com") - in this course we will use Google's Firebase to host our frontend. 

Our frontend, like most frontends, must fetch relevant data to show the user. For example, when a user searches for a video on YouTube, YouTube's frontend will need to fetch the appropriate search results to render onto the users browser. This is where the backend comes into play. 

The backend part of our web application is responsible for listening to requests from the frontend and responds by providing the relevant processed data. Our backend will use Express.js to configure the endpoints at which the backend can service specific requests. For example, "http/my-web-app.com/search" might be configured to respond so that the frontend can process all search requests by issuing a HTTP POST request to that endpoint. We will use Node.js with TypeScript to implement our backend logic.

:::warning YELLOW
**Note:** You will only implement the orange bordered sections in this lab. In Assignment 1, you will be creating your own Mongo database!
:::



## HTTP Requests

The Hypertext Transfer Protocol (HTTP) is designed to enable communication between clients and servers. An HTTP request is made by a client to a named host, which is located on a server. The aim of the request is to access a resource on the server.

In this assignment, you will use four different types of HTTP requests:



| HTTP Request | Explanation | Example (let us say the URL/base endpoint is https://example.com) |
| -------- | -------- | -------- |
| GET    | Used for retrieving data     | https://example.com/node/get/:nodeid     |
| POST    | Used for create methods     | https://example.com/node/create     |
| PUT    | Used for update or replace methods     | https://example.com/node/move/:nodeId/:newParentId     |
| DELETE    | Used for deletion of data.    | https://example.com/node/:nodeid     |

:::info
**URL params:** The `:` indicates that the following value is a URL parameter. In other words, `:nodeId` is a placeholder that indicates a `nodeId` should be placed in its stead.
:::
:::warning
**Note:** The definitions and use-cases for each of these request types are flexible. There are industry standards and guidelines but at this stage, we don't need to know anything more than what is shown in the table above.
:::

### Asynchronous Functions

JavaScript is single-threaded, which means it only has one call stack. Using asynchronous functions, we can make time-intensive network requests without blocking the call stack. 

**What is "asynchronous"?**

Simply put, it means that JavaScript can move on to the next line of code before the previous line finishes executing. Here is an example:
```typescript=
console.log('first print') // print to console
setTimeout(() => console.log('second print'), 3000) // wait for 3 seconds
console.log('third print') // print to console
```
If you run this snippet of code in an editor, the console would produce the following message:
```typescript=
first print
third print
second print
```

When we tell JavaScript to hold off for 3 seconds to print the `second print`, JavaScript didn't wait. JavaScript added it into its "task list", marked it with *execute this task 3 seconds later*, and went ahead to execute the third task, which is printing `third print`. `third print` got printed immediately, and that's why `second print` comes after the `third print`. With the same reasoning, do you think the following snippet would achieve it's goal?

```typescript=
// takes 2s to return a list of GitHub repos in String
const apiResult = fetch('https://api.github.com/repos')
// printing it to console
console.log(apiResult)
```

In fact, it doesn't do what we want. Instead, it printed out 
`[object Promise] { ... }` in the console. Why? Becuase JavaScript didn't wait, and printed out whatever `apiResult` was before the fetch completed. You see, `apiResult` is a Promise - a Promise is effectively a wrapper around the **actual return type**, denoting that JavaScript is expecting a object to be returned, and *promises* that the object will either be returned (resolved), or it will raise an error (rejected). It takes time to get the Promise resolved. In TypeScript, we denote a promise by `Promise<T>`, where `T` refers to some generic type (could be a `string`, `object`, `INode`, etc.).

### `async` function & `await` keyword

:::info
`async` and `await` allow you to write asynchronous code the same way you write synchonous code
:::

#### `async` function
First of all we have the `async` keyword, which you put in front of a function declaration to turn it into an async function.

:::warning
:::spoiler Example of an `async` function vs regular function

**Regular function**:
```typescript=
const donutPun = (): string => { 
  return "I donut know any puns..." 
};
donutPun();
```

**`async` Typescript function**:
```typescript=
const donutPun = async (): Promise<string> => { 
  return "I donut know any puns..." };
}
donutPun();
```
:::

This is one of the traits of async functions — their return values are guaranteed to be converted to promises. In the second example, the return value would be of type `Promise<string>`. So the async keyword is added to functions to tell them to return a promise rather than directly returning the value.

#### `await` keyword

<!-- Need to rewrite this section -->
The advantage of an `async` function only becomes apparent when you combine it with the await keyword. `await` only works inside `async` functions. 

`await` can be put in front of any async promise-based function to pause your code on that line until the promise fulfills, then return the resulting value.

Adding `await` in front of a function, for example when we are retrieving from the database, is essentially telling JavaScript to **wait until you have a response** from the promise.

Now, you should know how to properly print the data that the GitHub API returns:

```typescript=
function fetchAPI = async () => {
    // with async/await, JavaScript will not execute the next line
    // until the fetch is completed
    const apiResult = await fetch('https://api.github.com/repos')
    // printing the correct result!
    console.log(apiResult)
}
```
You can learn more about the JavaScript event loop here: [What the heck is the event loop anyway?](https://www.youtube.com/watch?v=8aGhZQkoFbQ)

## Types

In the `src` folder on both the `server` and `client` you will find a `types` folder. This contains all of the interfaces that our hypermedia system will use. 

:::info
**Note**: We will be using a naming convention where all typescript interfaces are prefaced with an `I`  for interface. This helps code readability when you have both the interface and the implementation of that interface referenced in one file.
:::

#### `INode`

A node is a generic representation of content in our hypertext corpus. In this assignment, we will only be implementing a generic node service which manages the location of nodes in a file tree, and supports basic content for `text` and `image` nodes. To be clear, we are making a notable design decision by choosing to organize nodes via a file tree. This isn’t the only option when building a hypertext system, but it is what you will be implementing in this assignment. At its core, any file tree is just a hierarchy of nodes. Let’s start with how we are defining “node” in the context of a our Hypertext corpus:

```typescript=
interface INode {
    title: string // user created node title
    type: NodeType // type of node that is created
    content: string // the content of the node
    nodeId: string // guid which contains the type as a prefix
    filePath: INodePath // the location of node in file tree
    dateCreated?: Date // optional creation date
}
```

:::info
The `?` in `dateCreated?` means that it is an optional field in the INode interface. This means that an INode object can either have that field or not. 
:::



#### `INodePath`

The node path type is a simple interface which allows you to easily access an INode's location. This is important, as you will frequently interact with node paths. Here is the definition for the INodePath interface:

```typescript=
// Note: all these strings are `nodeId`s
interface INodePath {
    path: string[]
    children: string[]
}
```

:::success
**Example**: 

a root node with nodeId `root` who has two children, `child1` and `child2` would have the following `nodePath`:

`
{    
    path: ['root'],
    children: ['child1, child2']
}
`

and the `child1` node will have the following `nodePath`:

`{ path: ['root', 'child1'], children: [] }`
:::

:::info
The `children` array will only contain the `nodeId`s of their immediate children - which excludes grandchildren or great grandchildren.
:::

#### `IServiceResponse`
The `IServiceResponse` is an interface we’ve created for this course to make it easier to understand if a function succeeds or fails. If a function fails, the `ServiceResponse` will be a `failureServiceResponse` that contain an error message. If it succeeds, the ServiceResponse will be a `successfulServiceResponse` that contains a payload of type `T`, which is a generic type that could be anything (eg. string, INode, etc.) indicated in the angular brackets.

The `IServiceResponse` interface enables us to easily determine if a function succeeded, and if it fails, to pass an error message to the calling function.

```typescript=
interface IServiceResponse<T> {
  success: boolean;  
  message: string; 
  payload: T; 
}
```

Here we see that if `createNode()` executes successfully, it will return a `ServiceResponse` with a payload of the created node (of type `INode`).
```typescript=
function createNode(node: INode): Promise<IServiceResponse<INode>>;
```


# Backend

## Backend File Structure

Once you `cd` into the `server` folder, you will find the following file structure. 

![](https://i.imgur.com/iEknkhz.png)

### `NodeRouter.ts`
On the backend, you will be using the Express router found in `NodeRouter.ts` to receive HTTP requests from the frontend, make corresponding changes to the database, and give the frontend updated data to show the user. Once receiving an HTTP request, the `NodeRouter` will call on an appropriate method in `BackendNodeGateway`. 

:::warning
**Note:** `NodeRouter` listens to all HTTP requests to `/node`, therefore `MyRouter.post('/create', async...{})` actually corresponds to `/node/create`. For a more detailed explanation, please look at `server/src/app.ts` to see how `NodeRouter` is configured.
:::

### `BackendNodeGateway.ts`
`BackendNodeGateway` handles requests from `NodeRouter`, and calls on methods in `NodeCollectionConnection` to interact with the database. It contains the complex logic to check whether the request is valid, before modifying the database. 

For example, before insertion, `BackendNodeGateway.createNode()` will check whether the database already contains a node with the same `nodeId`, as well as the the validity of node's file path. The `NodeCollectionConnection.insertNode()` method, on the otherhand, simply receives the node object, and inserts it into the database.

### `NodeCollectionConnection.ts`
`NodeCollectionConnection` acts as an in-between communicator between the `nodes` collection in the MongoDB database and `NodeGateway`. `NodeCollectionConnection` defines methods that interact directly with MongoDB. That said, it does not include any of the complex logic that NodeGateway has.

For example, `NodeCollectionConnection.deleteNode()` will only delete a single node. Whereas `NodeGateway.deleteNode()` deletes all its children from the database as well.

:::warning
**Note:** In the backend part of this lab you will implement `/node/create` in `NodeRouter.ts`, `createNode` in `BackendNodeGateway.ts` and `insertNode` in `nodeCollectionConnection.ts`. This is to give you a better understanding of the Express, Node.js and MongoDB stack end-to-end. In Assignment 1 however, to make the workload more managable, you will only be required to implement methods in `NodeGateway.ts` as it handles the majority of the logic.
:::

## Step 1: Connect Backend to MongoDB

To permanently store our Hypermedia nodes such that they can be accessed using our web application no matter what computer they are on, we need to use a database service. In this class, we will be using MongoDB to store our data.

For this lab, you will be connecting to a database that we have created for you - a database that is shared with everyone in the course. In Assignment 1, you will create and deploy your own MongoDB database!

:::success
**TODO:**
1. `cd` into your `server` directory and add a `.env` file such that its filepath is: `server/.env`
2. Copy and paste the following text into this newly created `.env` file:

```
DB_URI = 'mongodb+srv://student:cs1951v@cs1951v-cluster.tjluq.mongodb.net/cs1951v-database?retryWrites=true&w=majority'
PORT=5000
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true
```
:::

:::danger
If you are running macOS Monterey or newer, you should set the port to 5001 instead of 5000. You will also need to make this change at the top of `FrontendNodeGateway.ts`
:::

So what did we just do with this `.env` file? Well, to connect to a database, MongoDB requires a URI that contains the unique address of the database, a username and password. This way, only those with the correct login details and unique database address can access a database. `DB_URI` in this `.env` file is that URI - 'student' is the username, 'cs1951v' is the password and '@cs1951v-cluster.tjluq.mongodb.net/cs1951v-database.' is our database's unique address. We store this information in a `.env` file so that this information does not lie in source code, code that is pushed online for potentially many to see. We use the `dotenv` node package to access the URI stored in this `.env`. Note, we have added `.env` to our `.gitignore` file so that private security details are not pushed online.


## Step 2: Verify that Stencil Code Runs

Before we start writing backend code, it's important check if your backend server runs:

:::success
**TODO**: 
Run the following `yarn` scripts to install the relevant dependencies and start the server:

`yarn install`
`yarn start`

To verify that your server is running, if you go to `http://localhost:5000` in your browser where you should see `MyHypermedia Backend Service`. If you don't see this message appear in your browser, please reach out to a TA!
:::

## Backend Implementation Roadmap

In this lab, you will be implementing the functionality for creating a node, starting with the backend and then finally rendering it as a React component on the frontend. The image below is a high level overview of how we will create a node on the backend.

<div style="text-align:center; margin-bottom: 30px; margin-top: 30px;"><img style="border-radius:5px; height:250px" src="https://i.imgur.com/yfZGJUZ.jpg" /></div>

In this lab, we want to insert both a text and image node into our database describing (debatably) the most delicious donut in the world, the Boston Cream Donut. 

<div style="text-align:center; margin-bottom: 20px"><img style="border-radius:5px; width:200px" src="https://i.imgur.com/eVujhw0.jpg" /></div>


## Step 3: Creating an INode JSON Object

Based on the INode interface that we introduced above, this is how a node like this would look when written as an object:

```json=
{
    "node": {
        "title": "Boston Cream Donut",
        "nodeId": "image.u1deL6",
        "type": "image",
        "content": "https://bakerbynature.com/wp-content/uploads/2021/08/Boston-Cream-Donuts-1-1-of-1.jpg",
        "filePath": {
            "path": [
                "image.u1deL6"
            ],
            "children": []
        }
    }
}
```

:::info
**Note**: `nodeId` is normally a randomly generated globally unique id (guid) on the client side; In this course, we use `image.<guid>` to represent some such unique id on an image node.
:::

:::success
**TODO**: 
1. Following the example above, create a `INode` object with `"type": "text"` (in an empty file), the content of it could be anything you would like, for example, it could be a bio that explains the Boston Cream Donut. The JSON should be formatted in the same way as the `Boston Cream Donut` INode shown above. 
    - Generate an *8 character* unique ID **[here](https://shortunique.id/)** to be used as the new guid. 
2. Give the `Boston Cream Donut` INode above a new guid such that:
    - `"nodeId": "image.<new-guid>"`
    - `"filePath": {"path": ["image.<new-guid>"], ...}`

Keep ahold of these JSON objects in a text file or anywhere that is most convenient for you - we will be needing them soon.
:::

So why did we just create these random JSON objects? Well, servers and clients communicate with each other over HTTP requests and responses all the time. Sometimes, they need to send data in the form of objects and we oftentimes use the JSON (JavaScript Object Notation) format to represent this data.

In this lab, we will be sending our backend these 2 JSON objects attached to separate HTTP requests. Our backend server will then read this data and convert these JSON objects into a proper `INode` object in our TypeScript environment.

Now, onto creating those HTTP requests to send to our backend!

## Step 4: HTTP requests with Postman

Postman is a free tool which helps developers run and debug API requests - in other words, it simulates a client and send HTTP requests to our NodeRouter. This is a quick and easy way to make HTTP requests when you don't have a client set up yet and want to interact with your backend server. 

:::success
**TODO**: 
1. Create an account
2. Download the [Postman desktop app](https://www.postman.com/downloads/).
3. Sign in to the Postman desktop app with the account you just created.
4. Navigate to `My Workspaces`, create a new collection called `cs1951v` and add a new request to this collection. Then copy the `image` INode JSON object from Step 3 and ensure your Postman setup looks as follows...
    - **Note**: remember to have changed the guid and have the `nodeId` and `filePath` fields reflect this change. Go back to `TODO #2` in Step 3 if you have not done so yet.


<div style="text-align:center; margin-bottom: 20px"><img style="border-radius:5px; height:400px" src="https://i.imgur.com/N3lpL5l.png" /></div>

**Your Postman should be set up such that the following is true**:
- You have `POST` selected as the request type
- The request URL is: `http://localhost:5000/node/create`
- You have `Body`, `raw`, and `JSON` selected in the respective places.

:::

:::success
**TODO**:
Once everything is set up - click `Send`. This will send the HTTP POST request to the backend server that you are running locally at `localhost:5000`. 

Oh no! You received a `FailureServiceResponse` object,`[NodeRouter] node/create not implemented`. Move on to Step 5 to make sure that our server accepts the POST request!
:::


## Step 5: `NodeRouter.ts`

To implement the post method on the Express router for `/node/create` navigate to `server/src/nodes/NodeRouter.ts`. You can see that nearly all methods in the file take in two parameters, `res` and `req`:
- `req` is a `Request` object that we receive from the client. 
- `res` is a `Response` object that we send back to the client indicating the status of the request along with an optional JSON payload that contains any requested information. 

As we saw in Postman (Step 4), we are sending a POST request to http://localhost:5000/node/create. Therefore we need to set up our router to listen to a POST request at http://localhost:5000/node/create:

```typescript=
MyRouter.post('/create', async (req: Request, res: Response) => {

})
```
:::warning
**Note:** `NodeRouter` listens to all HTTP requests to `/node`, therefore `MyRouter.post('/create', async...{})` actually corresponds to `/node/create`. For a more detailed explanation, please look at `server/src/app.ts` to see how `NodeRouter` is configured.
:::

Now that we have the router listening to the proper route, let's read the JSON object that was attached to our Postman's POST request: 
```typescript=
MyRouter.post('/create', async (req: Request, res: Response) => {
  const node = req.body.node
})
```
::: info
**Using `console.log()` to verify that `node` has been received:**

1. insert `console.log(node)` after line 2
2. start the server with `cd server`, `yarn install` and `yarn start`
3. send the POST request via Postman, you should see the JSON object appear like this: 
```json=
{
  title: 'Boston Cream Donut',
  nodeId: 'image.guidasdf',
  type: 'image',
  content: 'https://bakerbynature.com/wp-content/uploads/2021/08/Boston-Cream-Donuts-1-1-of-1.jpg',
  filePath: { path: [ 'image.guidasdf' ], children: [] }
}
```
Awesome! Our HTTP POST request sent via Postman was received and our NodeRouter has succesfully parsed the JSON INode object attached to the POST request.
:::

Now let's check to see if `node` is of type node. We use `isINode()` to do this. 

```typescript=
MyRouter.post('/create', async (req: Request, res: Response) => {
  const node = req.body.node
  if (!isINode(node)) {
    // we want to send a failure status here
  } else {
    // we want to call a nodeGateway method to here help us 
    // create and insert the node
  }
})
```

Let's fill in the HTTP status code into our response.
```typescript=
MyRouter.post('/create', async (req: Request, res: Response) => {
  const node = req.body.node
  if (!isINode(node)) {
    // 400 Bad Request
    res.status(400).send('not INode!')
  } else {
    // 200 OK
    res.status(200).send('is INode!')
  }
})
```

Now, if the node is an INode, we should call on `NodeGateway` method to insert it into database.
```typescript=
MyRouter.post('/create', async (req: Request, res: Response) => {
  const node = req.body.node
  console.log('[NodeRouter] create')
  if (!isINode(node)) {
    res.status(400).send('not INode!')
  } else {
    // call `NodeGateway` method
    const response = await this.nodeGateway.createNode(node)
    res.status(200).send(response)
  }
})
```

Almost there - to make sure our router doesn't fail on unexpected errors, let's add a `try/catch` to make sure (`try/catch` is a common programming pattern that allows catching and handling errors).

```typescript=
MyRouter.post('/create', async (req: Request, res: Response) => {
  try {
    const node = req.body.node
    console.log('[NodeRouter] create')
    if (!isINode(node)) {
      res.status(400).send('not INode!')
    } else {
      const response = await this.nodeGateway.createNode(node)
      res.status(200).send(response)
    }
  } catch (e) {
    // 500 Internal Server Error
    res.status(500).send(e.message)
  }
})
```

For our NodeRouter methods we want to use a `try/catch` block so we can catch any uncaught errors and return that error message to the frontend. Note, for all caught errors, our backend should return a `failureServiceResponse`. 

:::info
**Note**: `failureServiceResponse` is a method  that we have created in `IServiceResponse.ts` to return an IServiceResponse object where `success: false`. The same is true for `successfulServiceResponse` except that `success: true`.

Please have a look at `server/src/types/IServiceResponse.ts` for the actual schema of IServiceResponse objects.
:::


## Step 6: BackendNodeGateway.ts
Now that we have written `/create` for NodeRouter and called `BackendNodeGateway.createNode()`, let's move onto the next step and write `BackendNodeGateway.createNode()`. 

We start with an empty method:
```typescript=
async createNode(node: any): Promise<IServiceResponse<INode>> {
  return failureServiceResponse(
      'TODO: [NodeGatway] createNode is not implemented'
    )
}
```
First, we want to check if our argument is of type INode. As a rule of thumb, each method is responsible for error checking its own inputs. Let's return a `failureServiceResponse` with an appropriate failure message if `node` is not a valid INode.
```typescript=
async createNode(node: any): Promise<IServiceResponse<INode>> {
  // check if node is valid INode
  const isValidNode = isINode(node)
  if (!isValidNode) {
    return failureServiceResponse('Not a valid node.')
  }
  return null
}
```
Next, we want to check if a node with `node.nodeId` already exists in the database. We don't want duplicate `nodeId`s in the database. Let's use `NodeCollectionConnection`'s `findNodeById()` method which has already been written for us to verify if `nodeId` already exists in the database. We return a `failureServiceResponse` if a duplicate already exists.
```typescript=
async createNode(node: any): Promise<IServiceResponse<INode>> {
  const isValidNode = isINode(node)
  if (!isValidNode) {
    return failureServiceResponse('Not a valid node.')
  }
  // check whether nodeId already in database
  const nodeResponse = await this.nodeCollectionConnection.findNodeById(
    node.nodeId
  )
  if (nodeResponse.success) {
    return failureServiceResponse(
      'Node with duplicate ID already exist in database.'
    )
  }
  return failureServiceResponse(
      'TODO: [NodeGatway] createNode is not implemented'
    )
}
```
The nodes are organized in a tree structure such that root nodes contain child nodes that then contain other child nodes, etc. This means that when we are creating a node, we want to make sure that the parent we will attach this new node exists. We can access the node's parentId by indexing `node.filePath.path[nodePath.path.length - 2]`. Note that root nodes will not have a `parentId`. Let's put this into code:
```typescript=
async createNode(node: any): Promise<IServiceResponse<INode>> {
  const isValidNode = isINode(node)
  if (!isValidNode) {
    return failureServiceResponse('Not a valid node.')
  }
  const nodeResponse = await this.nodeCollectionConnection.findNodeById(
    node.nodeId
  )
  if (nodeResponse.success) {
    return failureServiceResponse(
      'Node with duplicate ID already exist in database.'
    )
  }
  // check if parent exists
  const nodePath = node.filePath
  // if node is not a root
  if (nodePath.path.length > 1) {
    const parentId = nodePath.path[nodePath.path.length - 2]
    const parentResponse = await this.nodeCollectionConnection.findNodeById(
      parentId
    )
    if (!parentResponse.success) {
      return failureServiceResponse('Node has invalid parent')
    }
  }
  return failureServiceResponse(
      'TODO: [NodeGatway] createNode is not implemented'
    )
}
```
Now if our node's `filePath` disagrees with its parent's `filePath`, we want to send a failure response:
```typescript=
async createNode(node: any): Promise<IServiceResponse<INode>> {
  const isValidNode = isINode(node)
  if (!isValidNode) {
    return failureServiceResponse('Not a valid node.')
  }
  const nodeResponse = await this.nodeCollectionConnection.findNodeById(
    node.nodeId
  )
  if (nodeResponse.success) {
    return failureServiceResponse(
      'Node with duplicate ID already exist in database.'
    )
  }
  const nodePath = node.filePath
  if (nodePath.path.length > 1) {
    const parentId = nodePath.path[nodePath.path.length - 2]
    const parentResponse = await this.nodeCollectionConnection.findNodeById(
      parentId
    )
    // if parent is not found, or parent has different file path
    if (
      !parentResponse.success ||
      parentResponse.payload.filePath.path.toString() !==
        nodePath.path.slice(0, -1).toString()
    ) {
      return failureServiceResponse('Node has invalid parent / file path.')
    }
  }
  return failureServiceResponse(
      'TODO: [NodeGatway] createNode is not implemented'
    )
}
```
Now we need to add `node.nodeId` to the parent's `children` field. 
```typescript=
async createNode(node: any): Promise<IServiceResponse<INode>> {
  const isValidNode = isINode(node)
  if (!isValidNode) {
    return failureServiceResponse('Not a valid node.')
  }
  const nodeResponse = await this.nodeCollectionConnection.findNodeById(
    node.nodeId
  )
  if (nodeResponse.success) {
    return failureServiceResponse(
      'Node with duplicate ID already exist in database.'
    )
  }
  const nodePath = node.filePath
  if (nodePath.path.length > 1) {
    const parentId = nodePath.path[nodePath.path.length - 2]
    const parentResponse = await this.nodeCollectionConnection.findNodeById(
      parentId
    )
    if (
      !parentResponse.success ||
      parentResponse.payload.filePath.path.toString() !==
        nodePath.path.slice(0, -1).toString()
    ) {
      return failureServiceResponse('Node has invalid parent / file path.')
    }
    // add nodeId to parent's filePath.children field
    parentResponse.payload.filePath.children.push(node.nodeId)
    const updateParentResp = await this.updateNode(
      parentResponse.payload.nodeId,
      [{ fieldName: 'filePath', value: parentResponse.payload.filePath }]
    )
    if (!updateParentResp.success) {
      return failureServiceResponse(
        'Failed to update parent.filePath.children.'
      )
    }
  }
  return failureServiceResponse(
      'TODO: [NodeGatway] createNode is not implemented'
    )
}
```
And finally, once all checks have been completed and the parent node's `filePath.children` field has been updated, we can insert this new node into the database using `NodeCollectionConnection.insertNode()`. We then  return the resulting response so `NodeCollectionConnection`'s response is passed onto `NodeRouter`.
```typescript=
async createNode(node: any): Promise<IServiceResponse<INode>> {
  const isValidNode = isINode(node)
  if (!isValidNode) {
    return failureServiceResponse('Not a valid node.')
  }
  const nodeResponse = await this.nodeCollectionConnection.findNodeById(
    node.nodeId
  )
  if (nodeResponse.success) {
    return failureServiceResponse(
      'Node with duplicate ID already exist in database.'
    )
  }
  const nodePath = node.filePath
  if (nodePath.path.length > 1) {
    const parentId = nodePath.path[nodePath.path.length - 2]
    const parentResponse = await this.nodeCollectionConnection.findNodeById(
      parentId
    )
    if (
      !parentResponse.success ||
      parentResponse.payload.filePath.path.toString() !==
        nodePath.path.slice(0, -1).toString()
    ) {
      return failureServiceResponse('Node has invalid parent / file path.')
    }
    parentResponse.payload.filePath.children.push(node.nodeId)
    const updateParentResp = await this.updateNode(
      parentResponse.payload.nodeId,
      [{ fieldName: 'filePath', value: parentResponse.payload.filePath }]
    )
    if (!updateParentResp.success) {
      return failureServiceResponse(
        'Failed to update parent.filePath.children.'
      )
    }
  }
  // if everything checks out, insert node
  const insertNodeResp = await this.nodeCollectionConnection.insertNode(node)
  return insertNodeResp
}
```
Great! We just finished `BackendNodeGateway.createNode()`. Let's move onto `NodeCollectionConnection.insertNode()`.

## Step 7: `NodeCollectionConnection.ts`
Last thing we need to do in the backend, is to interface with the database! Let's start by checking whether the input is a valid `INode`.
```typescript=
async insertNode(node: INode): Promise<IServiceResponse<INode>> {
  // check to see that node is valid INode
  if (!isINode(node)) {
    return failureServiceResponse(
      'Failed to insert node due to improper input ' +
      'to nodeCollectionConnection.insertNode'
    )
  }
}
```
After this, let's call the relavant MongoDB function, and return the inserted document on success.
```typescript=
async insertNode(node: INode): Promise<IServiceResponse<INode>> {
    if (!isINode(node)) {
      return failureServiceResponse(
        'Failed to insert node due to improper input ' +
          'to nodeCollectionConnection.insertNode'
      )
    }
    // call mongodb function to insert node and return the inserted node
    const insertResponse = await this.client
      .db()
      .collection(this.collectionName)
      .insertOne(node)
    if (insertResponse.insertedCount) {
      return successfulServiceResponse(insertResponse.ops[0])
    }
    return failureServiceResponse(
      'Failed to insert node, insertCount: ' + insertResponse.insertedCount
    )
  }
```
<!-- ## Step 8: Run Backend Test
Great! So now that we have fully implemented our system's create node functionality, let's run some unit tests to see if our code works as intended. 
:::info
**Backend Tests:** There are two types of tests: system tests and unit tests. 
- System tests test our entire system end-to-end (e2e) as if we were using the system as a user. This interacts with a real MongoDB database to simulate real world use. System tests can be found at `server/src/test/Nodes/e2e`.
- Unit tests test each method in our program one by one such that we are absolutely sure that each method works as intended. We normally only test public/exposed methods - helper methods will not be unit tested. Unit tests can be found at `server/src/test/Nodes/Mock`. Note, we use 'Mock' as it symbolizes the 'Mock' database we use for unit testing - a virtual database we simulate in our computer's local memory.
:::

:::success
**TODO:**
1. Run `cd server` and `yarn labtest` in your terminal to see whether it passes the tests!
:::

If all tests pass, you are all set on the backend! Now let's move onto the frontend portion of this lab. -->
# Frontend

## Introduction

Now that we have a working backend that accepts INode objects from HTTP requests, we should make sure that our client is able to send those HTTP requests. 

Once we have a way of creating nodes, we will be implementing two simple React components in this lab to introduce a few key concepts: props, rendering, and components as functions.

### Frontend Folder Structure

![](https://i.imgur.com/xiVNU7N.png)

The blue elements are what you will be working on in this lab and assignment. **You can search for a file in VSCode using Cmd+P (Mac) or Ctrl+P (Windows).**


### React

React is a frontend Javascript library for building isolated and state-aware UI components. This makes it ideal for building high performance modern web applications.

Some examples of webpages that you may have used that use React include:
- Facebook
- Instagram
- Netflix
- Twitter
- Reddit

If you're new to React or want a refresher, you might find [this](https://beta.reactjs.org/learn) tutorial helpful.

#### Component Breakdown

A Component is one of the core building blocks of React. In other words, we can say that every application you will develop in React will be made up of pieces called components. Components make the task of building UIs much easier. You can see a UI broken down into multiple individual pieces called components and work on them independently and merge them all in a parent component which will be your final UI.

The beauty of React is that when we change the state of a Component, rather than re-rendering the entire UI's state, it only updates the Component's state. This is what we mean by Component isolation.

In this image we have broken down the React UI Components of the MyHypermedia Web App that you will be implementing. 

![](https://i.imgur.com/UDOw4vu.png)

In our stencil code, each React component is organized into a folder. Let us say we had a `Donut` component. Then it would be a folder called `Donut` which contains the following files:


| **Filename** | **Case Style** | **Description** |
| -------- | -------- | -------- |
| Donut.tsx     | PascalCase    | Handles the rendering of the component itself and the creation of the JSX object.    |
| donutUtils.ts/.tsx     | camelCase    | Code that does not depend on `useState` or `useEffect` is factored into a utils file.     |
| Donut.scss     | PascalCase    | SCSS file with everything relevant to the Donut component that is rendered.     |
| index.ts     | camelCase    | File that exports all of the relevant functions & components to avoid lengthy and messy import calls. (eg. `import { oreo } from '../../../../../../../donuts/chocolate/oreo'`)     |



## Verify that your frontend loads

First we should add the `.env` to your `client` folder so that ESLint works as we expect it to! It should look as follows:
```
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true
```

Before we start writing frontend code, it's important to check that your client runs as expected.

:::success
**TODO**: 
Run the following `yarn` scripts to install the relevant dependencies and start the React services:
`cd client`
`yarn install`
`yarn start`

To verify that your React service is running, if you go to `https://localhost:3000` in your browser then you should see the client web app that we have built `MyHypermedia`. If the web app does not load, please shout "TAaaa!!" (in Slack or in CIT219)
:::

## Step 1: `createNode` in `FrontendNodeGateway.ts`

We want to easily send and retrieve data from our database using our frontend app. Users will interact with data through buttons, forms etc. and we need a way to retrieve data. For that we will use `FrontendNodeGateway` to make HTTP requests.

We use **[Axios](https://axios-http.com/docs/intro)** to make our HTTP requests, which offers an easier and more powerful way to make requests compared to the more basic Fetch API. All our requests are contained in `requests.ts`. For `createNode` we want to use a POST method since we are sending data from the client to the server!


```typescript=
createNode: async (node: INode): Promise<IServiceResponse<INode>> => {
    try {
      /**
       * Make a HTTP POST request which includes:
       * - url: string
       * - data: the data attached to the HTTP request
       */
      const url:string = ??
      const body:any = ??
      return await post<IServiceResponse<INode>>(url, body)
    } catch (exception) {
      return failureServiceResponse('[createNode] Unable to access backend')
    }
}
```

Now we need to define the arguments that we are passing into the post request:
- **`url`**: 
    When you are running it locally the `baseEndpoint` variable `https://localhost:5000`. 

    Since this `NodeGateway` only sends HTTP requests to `/node...` we use the `servicePath` variable to define the permanent prefix attached to the route.
        
    Thus, our url should be:
    `baseEndpoint + servicePath + '/create'`

- **`body`**:
    The body of the POST HTTP request should be the INode object that we are creating.
    

Finally, we arrive at the final method, which returns a `IServiceResponse` Promise. 

```typescript=
createNode: async (node: INode): Promise<IServiceResponse<INode>> => {
    try {
      const url: string = baseEndpoint + servicePath + '/create'
      const body: any = { node: node }
      return await post<IServiceResponse<INode>>(url, body)
    } catch (exception) {
      return failureServiceResponse('[createNode] Unable to access backend')
    }
}
```

We can now access that from anywhere in our `client` to make an API request to create a node!

```typescript=
import NodeGateway from '../../nodes/NodeGateway'
const nodeResponse = await NodeGateway.createNode(newNode)
```

**You can look into the `createNodeFromModal` method in `createNodeUtils.ts` to see an example!**


## Step 2: ReactHooks & opening the CreateNodeModal

Before we do this step, we will introduce two **very** important hooks in React!

:::warning
We will be using Functional Components with React Hooks in this course as opposed to the traditional Class Components in React. We do this because Functional Component requires much less boilerplate code, and is now the  industry standard.
:::

### React Hooks
First, let's talk about React Hooks. You can identify a React Hook by its names - they usually have `use` as a prefix to their name. Essentially, React Hooks deal with manipulating the state, or the value, of the variable. With hooks, React will automatically rerender (update) the components that contain this variable. 

#### useState
`useState` is the most common hook, and it deals with creating reactive variables - whenever reactive variables get updated, React will automatically re-render the component with the updated value. Imagine we want to have a `hitCounter` variable that represents the number of button clicks. We can declare it with the `useState` hook:

```typescript=
//     variable    mutator of variable       initial value
const [hitCounter, setHitCounter] = useState(0)
```

The first word, `const`, is the reserved keyword for declaring a new constant variable. However, we are actually declaring 2 things here: a `hitCounter` variable of type `number`, and a function `setHitCounter` that is used to mutate the value of `hitCounter`. Both of these are returned from the useState hook. On the right side of the equal sign, we are calling the `useState` hook with the parameter `0` - that indicates that the `hitCounter` will be initialized at the value of `0`. 

So we've used `useState`, is that it? Let's see how we can use these 2 variables to achieve our goal - building a hit counter!

```typescript=
import React, { useState } from "react";

const myCounter = () => {
  // using the React Hook here
  const [hitCounter, setHitCounter] = useState(0);

  return (
    <div>
      <div>Count: {hitCounter}</div>
      <button onClick={() => setHitCounter(hitCounter + 1)}>
        Hit Me!
      </button>
    <div>
  );
};
```

Can you visualize what it would look like? If rendered in browser, there would probably be a `Count: 0` along with a button that says `Hit Me!`. Let's observe what is written inside the `<button>`.

We see that there is an `onClick` event handler written inside the `<button>` div. Whatever is written inside the `onClick` will be called when user clicks the button. In this case, we are calling `setHitCounter` with the parameter `hitCounter + 1` to increment `hitCounter` everytime this button is pressed. `hitCounter`'s current value is `0`. With some math knowledge, we know that we are effectively putting `1` as an argument into the `setHitCounter`; not surprisingly, React will now update `hitCounter`'s value to `1`, and the DOM will show `Count: 1`. If we keep clicking the button, this process will be repeated, and we can see the `hitCounter` value to steadily increase.

To recap, `useState` instantiates a variable with a default value, and React provides us with a mutator to change the variable's value. Every update to variable will cause a rerender of the component.

:::warning
Note that the value of the `hitCounter` will be restored to 0 when we refresh the page. That is the anticipated behavior. If we want to make the data persistent through page refresh, we can use a [database](https://www.mongodb.com/what-is-mongodb), or utilize the browser's [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
:::

**Global State Management**
You might notice that the stencil code sometimes uses `useRecoilState` instead of `useState`. This is because large applications with many state variables that need to be accessed by multiple components can quickly become cluttered and buggy. 

That's where global state management comes in! The Recoil state management system uses the same basic principles as useState hooks, but allows for easy access to the same state variable across components. Global state variables are defined in atom.tsx, and to use them within a component, we can use:
- `useRecoilState` to access the value and mutator
- `useRecoilValue` to only access the value
- `useSetRecoilState` to only access the mutator

#### useEffect
Now, let's learn another simple hook - `useEffect`. We can treat this hook as an `if changed` statement. Let's see what the `useEffect` hook looks like.

```typescript=
//                  callback function              dependency array      
useEffect(() => { console.log('Button Clicked!') }, [hitCounter])
```

For readability, we formatted the `useEffect` into one-line and commented on each part. A `useEffect` hook takes in 2 parameters, the first one being a [*callback function*](https://stackoverflow.com/questions/824234/what-is-a-callback-function), and the second parameter is a dependency array.

:::warning
**Callback Function**

A callback function is a function that's passed as an argument to another function.
:::
:::warning
**Dependency Array**

A dependency array is essentially the `if` part of the `useEffect` hook. We can put variables of different types into the dependency array, and whenever **any** variable in the dependency array gets updated, the callback function will be executed. For example, in the code snippet above, whenever `hitCounter` is updated, the callback function will print to the console. You can put as many variables as you want into the dependency array.

There are 2 special cases for the dependency array:
- When the dependency array is empty, such as `useEffect(() => { console.log("hello!) }, [])`, it means the callback function will only be called **once** on component creation. When we want to make an API call when the page / component loads, this mode comes in handy.
- When the dependency array is not supplied, such as `useEffect(() => { console.log("change!")})`, the callback function will be executed whenever the component is rerendered. 
:::

With this knowledge, what does this snippet of code to do?
```typescript=
import React, { useState, useEffect } from "react";

const myCounter = () => {
  // using the React Hook here
  const [hitCounter, setHitCounter] = useState(0);

  useEffect(() => {
    console.log("Clicked!")
  }, [hitCounter])
  
  return (
    <div>
      <div>Count: {hitCounter}</div>
      <button onClick={() => setHitCounter(hitCounter + 1)}>
        Hit Me!
      </button>
    <div>
  );
};
```

Whenever the button is clicked, the value of the `hitCounter` will be changed, triggering a useEffect call. The useEffect will print a string to console.

:::info
**Basic Rules of React Hooks**

1. Hooks can only be used in functional component; they're not compatible with the old class components.
3. Hooks cannot be called in loops, `if` conditions, or nested functions.
:::


Now that we've gone over useState and useEffect hooks, let's go back to the codebase and implement the `create` button to open the modal. We use a UI package called **[Chakra](https://chakra-ui.com/docs/overlay/modal)** to render our createNodeModal.

Navigate to `MainView.tsx`, this is where we render the components and handle logic to load all of the components and the nodes from our database. We have divided the file into 6 sections, they are as follows:
- [1] useState hooks
- [2] Constant variables
- [3] useEffect hooks
- [4] Button handlers
- [5] Helper functions
- [6] JSX component
 

**Step 1**: Notice the following useState hook in **[1]**:

```typescript=
const [createNodeModalOpen, setCreateNodeModalOpen] = useState(false)
```

We use a state variable called `createNodeModalOpen` to keep track of state of the `CreateNodeModal`.

If the `createNodeModalOpen` variable changes, our MainView component will know to re-render the components that depend on the `createNodeModalOpen`, but how do we change the variable? For that we need to use the `setCreateNodeModdalOpen` function from our useState hook which updates the variable! 

**Step 2**: Go to **[6] JSX Component** in the file. Since the createNode button is located in the `Header` component we need the `Header` component to be able to access and call the `setCreateNodeModalOpen` function. How do we do this? We pass it in to the component as a `prop`, one of the arguments of the component.

:::info
We'll go into more detail on components and props in Step 3!
:::

We should update the `onCreateNodeButtonClick` prop in the `Header` component so that it looks as follows: 

```typescript=
 <Header
    onHomeClick={handleHomeClick}
    onCreateNodeButtonClick={() => setCreateNodeModalOpen(true)}
    nodeIdsToNodesMap={nodeIdsToNodesMap}
/>
```

Now our `Header` component takes in the method that updates our variable. It knows what to do when the create button is clicked! Try it out, and try creating a node with the CreateNodeModal!

**Congratulations!** You've successfully implemented the **Create Node** functionality, from the backend all the way to the frontend!


## Step 3: Implementing ImageContent.tsx

Now that we are able to create a node, what about viewing it? Sure, we have an INode object with fields, but how useful is that? 

This is what our `Boston Cream Donut` currently looks like; not so delicious:

<div style="text-align:center; margin-bottom: 20px"><img style="border-radius:5px; width:600px; border: solid black 1px" src="https://i.imgur.com/zQkTx6N.png" /></div>


We need to take the INode oject and turn it into something useful. Start by navigating to `ImageContent.tsx`.  

**Here, we see the basic structure of a React component: it's just a function!** It takes an input  `props` that is an object of shape `IContentProps`, and it outputs what it should render (for now, `null`). 

`IContentProps` is actually defined in a different file; you can Cmd+Click (Mac) or Ctrl+Click (Windows) on almost any expression in VSCode to see where it's defined or used. A lot of people find this extremely useful; try it out!

You can see that `IContentProps` has a field called `content` of type `string`. For an image node, this string is the **image url**. 

A common pattern when programming in React is to get the properties you need from a `props` object. Here are two ways to get the `content` field from our `props`:

```typescript=
/** Option 1: Directly access individual properties */
const content = props.content

/**
 * Option 2: Use object destructuring.
 * This is equivalent to the above line! 
 */
const { content } = props

// You can use destructuring to access
// multiple properties at once, so it's
// generally preferred
// For example:
const { property1, property2 } = myObject

```

Once you have your `content` stored in a `const`, you will want to render an image. You can do this by returning an `<img />` tag with the `src` set to our image url (which is our `content`!).

It might surprise you to see an 'HTML tag' in a Javascript function. This is really a [JSX](https://reactjs.org/docs/introducing-jsx.html) tag rather than an HTML tag. JSX is an extension of Javascript that is similar to HTML, but also allows for Javascript expressions. We use `{...}` to denote Javascript when we are within a JSX element.

For example:

```typescript=
return (
    <img src={/** Put any JS value in these curly braces */} />
)
```

:::info
**Did you notice that `<img>` tag can be written in 2 formats?**

In JSX, which React uses, you can either use a traditional `<img src='...'></img>` to render an image, or to use a *self-closing* tag that looks like `<img src='...' />`. These are equivalent expressions and either format is accepted.
:::


## Step 4: Implementing TextContent.tsx

Now we can implement our text content component in  `TextContent.tsx`. The content of text nodes is simpler to render: it's just a `string`! You can render a Javascript `string` by wrapping it in curly braces and putting it in an outer tag (like a `div`).

For example:

```typescript=
const myString = 'donut!'
return (
    <div>{myString}</div>
)
```

Once you render your text content, you should be see the content text when you click on a text node. Now, you'll get some practice styling react components using `.scss` files!


:::info
**Styling with SCSS (.scss)**
Throughout this class we will be using `.scss` files instead of .css files.


SCSS files allows us to write SASS, which is an extension of CSS (Cascading Style Sheets) that adds functionality like variables and nesting. Because of these additional features, SCSS files are often used instead of CSS files in industry.


**Note**: While there are other alternatives for styling your React components, we'll be sticking with SCSS for this course. You can use other CSS styling methods such as `tailwind` and `emotion` if you choose, but TAs won't be able to offer support for these.
:::

In the same directory, go to `TextContent.scss`. Define a SCSS class for your text content -- the syntax is like this (though the class can be named anything):

```css=
.textContent {
  
}
```
Now, hook up this SCSS class to your React component by adding a `className` string to the HTML element you are rendering. You can do this like so:

```typescript=
return (
    <div className="<YOUR_CLASSNAME_HERE>">{myString}</div>
)
```

Ok, you're now ready to start styling! Some good places to start are `font-size`, `font-weight`, `color`, and `padding`. You can add styles like so:

```css=
.textContent {
    font-size: 10px;
    font-weight: bold;
    color: red;
    padding: 20px;
}
```

Change these values and add more styles (See https://www.w3schools.com/html/html_styles.asp for some more examples, or do a quick Google search!) until you are happy with how your text is rendered.

## Step 5: Rendering ImageContent.tsx and TextContent.tsx
Now that you have implemented `TextContent.tsx` and `ImageContent.tsx`, we need our `NodeView.tsx` to render either a `TextContent` or `ImageContent` component depending on the type of node selected. 

In `client/src/components/NodeView/NodeView.tsx` we see this snippet of code returns what node content to render:
```typescript=
// ...more code above
    <div
      className="nodeView-content"
      style={{
        maxHeight: hasBreadcrumb ? 'calc(100% - 118px)' : 'calc(100% - 72px)',
      }}
    >
      // this renders the node's content based on the node's type
      {renderContent(props, type, content)}
      </div>
    </div>
  )
}
// end of file
```
Navigate to the implementation of `renderContent` by `cmd + click` on Mac and `ctrl + click` on Windows. 

Implement the two `TODO`s in this file (`nodeViewUtils.tsx`). Pay attention to what props `ImageContent` and `TextContent` require!

Once done, your frontend should look like this!

![](https://i.imgur.com/4uKSGrV.jpg)

:::info
**Yay**! Now you can render the content of `text` and `image` nodes from our Mongo database!
:::

### <span style="border-bottom: solid 5px aliceblue;">Congrats! You're done with the lab; see a TA to get checked off. </span> 

![](https://i.imgur.com/Ef3XOBk.png)
