---
title: Lab 2 | Anchors and Links
tags: lab
---
<span style="font-size: 50px;">**Lab 2: Anchors and Links**</span>
:::info
**Released: September 29th**
**Due: October 5th 11:59pm ET**
To get checked off for this lab, meet with a TA at your lab section or TA hours
:::
:::danger
⚠️ **Do not clone the assignment 2 stencil code until you have submitted assignment 1 for the last time to Gradescope**. We will be checking to make sure you do not submit assignment 1 after cloning assignment 2. ⚠️
:::
<!-- 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**
In the first assignment, we created a basic hypertext system. Now we want to have more in-depth ways of interacting with our nodes. One of the features common to all hypertext systems is the ability to create two objects we will be adding to our system: anchors and links.
As with the node objects you created in the last assignment, each part of the stack will have a way of interacting with these new objects. Your completed assignment will contain the following for both Anchors and Links: database connections, gateways, routers, and frontend components.
In this lab, we are laying out the foundations to implement linking and anchoring. Now that you have all completed a basic file system and gone through the complexities of understanding the backend structure for our nodes, this unit will be more focused on frontend work, and will involve many more design decisions!
As always, if you have any questions about anything please reach out to a TA!
:::success
**Objective:** Introduce you to the new types and some techniques for frontend development that you will be working with for this assignment.
:::

## **Checklist**
- [ ] Complete the Assignment 1 feedback form
- [ ] Clone the repo for this lab
- [ ] Implement `linkCollectionConnection.deleteLinks()`
- [ ] Working with text,images and selected `extents`!
- [ ] Get checked off by the due date
# Reflecting on Assignment 1
You successfully prepped your donut! Now it's time to look over the recipe to see where you're at. Please complete this quick [feedback form](https://docs.google.com/forms/d/e/1FAIpQLSe8L2yK53mAvFeVR4IqZ8ckSHoOqiHF9YxvUT1FoavUe5Pfow/viewform?usp=sf_link) for Assignment 1. We want to get a good sense of what has gone well and what hasn't, so we can improve on future assignments.
# Cloning the GitHub Repo
You can clone the repository [here](https://classroom.github.com/a/43WvHq20).
:::success
Note: The same repo will be used for Lab 2 and Assignment 2!
:::
# New Types - ILink & IAnchor
We **highly** recommend that you look over the files in `server/src/types` as all of the following types are defined there:
* `IServiceResponse`
* `INode`
* `INodeProperties`
* `INodePath`
* `RecursiveNodeTree`
* `IAnchor`
* `Extent`
* `ILink`
We also provide and define useful helper methods related to these types in each of the files. Looking over these now will make your life much easier when you're looking for helper functions in the future.
## Anchors
In the previous assignment, you implemented the file system for nodes, but didn’t actually implement anything regarding selecting content on a node. In order for anchors to work properly, you need to be able to make selections on the content of a node, whether that node is a text document, an image, or a video. Conceptually, you can think about implementing anchors via two distinct components:
1) a generic component that manages all types of anchors and stores the associated node and other metadata about the anchor (`IAnchor`), and
2) specific implementations of anchors that define the location of that anchor for the given node type (`Extent`).
We have defined a schema for anchors as the `IAnchor` interface:
```typescript=
export interface IAnchor {
anchorId: string
nodeId: string
// Defines the extent of the anchor in the document,
// e.g. start / end characters in a text node.
// If extent is null, the anchor points to the node as a whole.
extent: Extent | null
}
```
and the`Extent` interfaces:
```typescript=
// Extent is 1 of 2 interfaces
export type Extent = ITextExtent | IImageExtent
// Defines the extent of an anchor on a text node
export interface ITextExtent {
type: 'text'
text: string
startCharacter: number
endCharacter: number
}
// Defines the extent of an anchor on an image node
export interface IImageExtent {
type: 'image'
// the following numbers will be explained later on
left: number
top: number
width: number
height: number
}
```
## Links
Once we have a way of creating anchors on our nodes, we need an intuitive way to navigate between them. This is where links come in! Whenever we place an anchor on the content of a node, we want to have the ability to link this anchor to another location. For example, if there is a statistic or quote within the content of a specific node, we want to have the ability to create an anchor on this section, and then link it to an anchor on the node from where this information comes.
The new `ILink` interface:
```typescript=
export interface ILink {
linkId: string
explainer: string
title: string
dateCreated?: Date
anchor1Id: string
anchor2Id: string
}
```
# Backend
#### Setting Up Environment Variables
You should know how to do this by now! Use the same `.env` file that you did for your own MongoDb connection in Unit 1 and add it into your `server` folder.
```bash=
DB_URI = <YOUR OWN URI>
PORT=5001
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true
```
If you change the port, make sure to also change the endpoint in the client's `endpoint.ts` file.
## MongoDB Queries
In Unit 1 you got your hands dirty with `BackendNodeGateway.ts` methods. In Unit 2, you will familiarize yourself with `AnchorCollectionConnection.ts` and `LinkCollectionConnection.ts`. These collection connection classes use the `mongodb` node package to interact with the MongoDB database we created in Assignment 1.
### But first, what exactly is MongoDB?
MongoDB is a cross-platform (runs on multiple operating systems), document-oriented database management system (DBMS). MongoDB is also a non-relational, NoSQL database. (SQL is a query language for relational databases, not a database architecture itself, so the NoSQL name is confusing, arguably the category should have been called non-relational.)
It is important to know at a high-level how this type of database operates, as the structure of a MongoDB database is inherently different from relational databases that use SQL. Traditional relational databases that use SQL to perform operations store data with predefined relationships. This is why these types of databases consist of a set of tables with columns and rows - to organize data points using defined relationships for easy access.
A non-relational, NoSQL database such as Mongo is different. Non-relational databases do not use the tabular schema of rows and columns found in most traditional database systems. Instead, data is stored as JSON-like objects with indexes that allow for constant lookup for certain fields. As a result, these types of databases tend to be more flexible by allowing data to be stored in myriad ways.
MongoDB uses documents that are in JSON-like format, known as BSON, which is the binary encoding of JSON. Node.js and MongoDB work very well together, in part because Mongo uses a JavaScript engine built into the database as JavaScript is good at handling JSON objects.
With Mongo being a non-relational database, it has its own way of storing data. Here are some of the constructs that make up the database structure:
- **Document**: A JSON-like object with a unique identifier that contains fields, which can store anything from strings and ints to arrays and complex objects. You can tell MongoDB which fields of a document to index for constant-time lookup and range queries. Documents do not have to have the same fields as one another, which is an advantage that Mongo databases have in flexibility over traditional, relational databases. [As Norm has said, the word “document” is confusing, since it has nothing to do with the types of documents (text, spreadsheets, etc.) that people are used to. We will continue to call them documents, but think of them as JSON-like objects.]
- **_id**: Mandatory unique field in every document. It separates one document from another, so we can identify each document independently. If this value is not provided, MongoDB automatically assigns a random value for the field.
- **Collection**: A set of documents. This is comparable to a table in a SQL database. However, unlike a SQL database, a collection does not have a set structure or pre-configured data types.
- **Database**: The container that holds a set of collections.
:::success
**Task 1**: First, navigate to `server/src/app.ts`. This file is what runs when you `yarn start`. As you see, we configure a `MongoClient` in this file as so:
:::
```typescript=
// access MongoDB URI from .env file
const uri = process.env.DB_URI
// instantiate new MongoClient with uri and our recommended settings
const mongoClient = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
// connect mongoClient to a MongoDB server
mongoClient.connect()
```
A few lines below this, we pass this `mongoClient` into each of our routers:
```typescript=
// instantiate new NodeRouter and pass mongoClient
const myNodeRouter = new NodeRouter(mongoClient)
// (ignore this: it attaches a new router to our express application)
app.use('/node', myNodeRouter.getExpressRouter())
// the same is done for the anchor and link routers
// [more code for anchor and link routers follow...]
```
This `mongoClient` is then passed down from the `router` > `gateway` > `collectionConnection` via their respective constructors. For example for links:
```typescript=
// In LinkRouter.ts:
constructor(mongoClient: MongoClient) {
this.linkGateway = new LinkGateway(mongoClient)
//...
}
// In LinkGateway.ts:
constructor(mongoClient: MongoClient, collectionName?: string) {
this.linkCollectionConnection = new LinkCollectionConnection(
mongoClient,
// If collectionName is undefined, collectionName will be
// 'links' by default. We use this for e2e testing when we
// create a separate 'e2e-testing-links' collection. You can
// think of the '??' operator as a safer alternative to '||'
collectionName ?? 'links'
)
}
// In LinkCollectionConnection.ts
constructor(mongoClient: MongoClient, collectionName?: string) {
// store mongoClient in this.client as an instance variable
this.client = mongoClient
// store collectionName in this.collectionName as an instance variable
this.collectionName = collectionName ?? 'links'
}
// We can now use the MongoClient from App.ts in LinkCollectionConnection
```
Now that we have the `MongoClient`, let's try to write `LinkCollectionConnection.deleteLinks()`.
:::success
**Task 2**: Implement `LinkCollectionConnection.deleteLinks()`
:::
First, we need to get the `links` collection:
```typescript=
/**
* Deletes links when given a list of linkIds.
* @param {string[]} linkIds
* @return successfulServiceResponse<{}> on success
* failureServiceResponse on failure
*/
async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> {
// note: earlier, in the constructor of linkCollectionConnection,
// we assign this.collectionName to 'links' and mongoClient to this.client
const collection = await this.client.db().collection(this.collectionName)
}
```
Next, we need to form a MongoDB query.
```typescript=
/**
* Deletes links when given a list of linkIds.
* @param {string[]} linkIds
* @return successfulServiceResponse<{}> on success
* failureServiceResponse on failure
*/
async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> {
const collection = await this.client.db().collection(this.collectionName)
// this query requests all documents where the field 'linkId' matches
// some element in 'linkIds'
const myQuery = { linkId: { $in: linkIds } }
}
```
:::info
**Learning MongoDB queries**
Over the duration of the semester, you will become familiar with MongoDB queries and will hopefully be able to form basic ones on your own. Here are some basic ones to get you started, but feel free to consult Google at anytime!
**Some Basic MongoDB Query Functions**
The stencil code already calls `getCollection` and stores the response in a variable. Below are some of the functions that you can call on the collection variable to interact with the MongoDB database. Feel free to reference the previous assignment’s backend `NodeCollectionConnection.ts` file to help with your implementation.
`collection.findOne(query)` - [Docs](https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#findOne)
`collection.find(query)` - [Docs](https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#find)
`collection.deleteOne(query)` - [Docs](https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#deleteOne)
`collection.deleteMany(query)` - [Docs](https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#deleteMany)
`collection.insertMany(query)` - [Docs](https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#insertMany)
`collection.insertOne(query)` - [Docs](https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#insertOne)
:::
Next, we need to call a MongoDB API call with our newly formed query. Notice that we call `await` on this method because we are sending a request the MongoDB server that we connected to using `mongoClient.connect()`. Because they are hosted on a remote server, we must then await a response from the MongoDB server before continuing our program.
```typescript=
/**
* Deletes links when given a list of linkIds.
* @param {string[]} linkIds
* @return successfulServiceResponse<ILink> on success
* failureServiceResponse on failure
*/
async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> {
const collection = await this.client.db().collection(this.collectionName)
const myQuery = { linkId: { $in: linkIds } }
// Here we use the 'deleteMany' function as we want to delete multiple
// documents that meet our query
const deleteResponse = await collection.deleteMany(myQuery)
}
```
Now that we have stored the `deleteResponse`, we have to verify whether or not the deletion was successful and return an IServiceResponse accordingly.
```typescript=
/**
* Deletes links when given a list of linkIds.
* @param {string[]} linkIds
* @return successfulServiceResponse<ILink> on success
* failureServiceResponse on failure
*/
async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> {
const collection = await this.client.db().collection(this.collectionName)
const myQuery = { linkId: { $in: linkIds } }
const deleteResponse = await collection.deleteMany(myQuery)
// we use result.ok to error check deleteMany
if (deleteResponse.result.ok) {
return successfulServiceResponse({})
}
return failureServiceResponse('Failed to delete links')
}
```
:::warning
:::spoiler **Error checking MongoDB responses**
Each MongoDB method call will return a differently shaped response. This means, that the method for error checking a `deleteMany` reponse may be different to the method for error checking a `findOneAndUpdate` method. For example, this is how we error check `findOneAndUpdate` in `NodeCollectionConnection.updateNode()`:
```typescript=
async updateNode(
nodeId: string,
updatedProperties: Object
): Promise<IServiceResponse<INode>> {
const collection = await this.client.db().collection(this.collectionName)
const updateResponse = collection.findOneAndUpdate(
{ nodeId: nodeId }, { $set: updatedProperties })
if (updateResponse.ok && updateResponse.lastErrorObject.n) {
return successfulServiceResponse(updateResponse.value)
}
return failureServiceResponse(
'Failed to update node, lastErrorObject: ' +
updateResponse.lastErrorObject.toString()
)
}
```
:::
:::info
We **highly** recommend looking at `NodeCollectionConnection.ts` for tips on how to form MongoDB queries, e.g. how to query a nested field, how to update documents, etc.
:::
# Frontend
## Environment Variables
Please add the following to your frontend's `.env` file (`client.env`).
```bash=
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true
```
## Improving performance in React
In this section, we will introduce a two more built-in React hooks that help reduce unnecessary rerendering. They essentially memoize your functions (`useCallback`) or your values (`useMemo`).
There are also a few **"Code Review"** info blocks that give software engineering tips and best practices, sort of like what you might receive from a code review at a company.
### useCallback
You may remember that a callback is simply a function passed as an argument. Whenever you pass a function as an argument, it's best to wrap it in a [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) to make sure it only updates when needed.
```typescript=
// Example without useCallback
const MyParentComponent = () => {
const [selectedNode, setSelectedNode] = useState<INode>(...);
const handleNodeClick = (node: INode) => {
setSelectedNode(node)
}
return (
<MyChildComponent onNodeClick={handleNodeClick} />
)
}
```
```typescript=
// Example with useCallback
const MyParentComponent = () => {
const [selectedNode, setSelectedNode] = useState<INode>(...);
const handleNodeClick = useCallback((node: INode) => {
setSelectedNode(node)
}, [setSelectedNode])
return (
<MyChildComponent onNodeClick={handleNodeClick} />
)
}
```
In the first example, a new reference to `handleNodeClick` is created every time `MyParentComponent` rerenders. Since we pass `handleNodeClick` as a prop to `MyChildComponent`, we will also make `MyChildComponent` rerender, which is inefficient. That's where `useCallback` comes in!
The first argument to `useCallback` is the function you want to wrap. The second argument is the dependency array, which is important and required. In the example above, the dependency array tells React to only change `handleNodeClick` if `setSelectedNode` changes.
:::info
**Code Review:** The example above also illustrates a common naming pattern in React. Our child component has a prop with a name like `onNounVerb` that will be called when some event happens in the child component (like a button click). We then write a function with a name like `handleNounVerb` that handles the event, which we pass into the child component.
:::
### useMemo
If we want to memoize a value instead of a callback function, we can use `useMemo`. It has a similar interface, including the dependency array:
```typescript=
const selectedNodeTitle: string = useMemo(() => {
return selectedNode.title;
}, [selectedNode])
```
In this example, `selectedNode` is a state variable, and we only want `selectedNodeTitle` to change when `selectedNode` changes. Often times, we will use `useMemo` to store values that we derive from state variables.
:::info
**Code Review:** As a rule of thumb, you should store as little data as possible in state variables, and derive your other data from the state. For instance, in the above example, we wouldn't want to store `selectedNodeTitle` as its own state variable, since we'd then have to remember to update it.
:::
Dependency arrays are very important! We have enabled a linter rule called `exhaustive-deps` that will make sure that every value that's used in the function is included in the dependency array, preventing a wide array of bugs.
For example, the following code would error:
```typescript=
const selectedNodeTitle: string = useMemo(() => {
return selectedNode.title;
}, [])
// eslint error, since selectedNodeTitle will not update
// if selectedNode changes
```
### useRef React Hook
useRef returns a mutable ref object whose .current property is initialized to the passed argument (`initialValue` in the example below). The returned object will persist for the full lifetime of the component.
```const refDonut = useRef(initialValue);```
Note that the initial value we pass into `useRef` is often `null`.
Here's a common use case for use ref, which you be using in later steps of this lab:
```typscript=
// this is a functional component called ChocolateDonut
function ChocolateDonut() {
const refDonut = useRef(null);
const handleMakeChocolateClick = () => {
// `current` points to the mounted div with className="donut"
refDonut.current.style.backgroundColor = "chocolate"
};
return (
<>
<div className="donut" ref={refDonut}/>
<Button icon={<fa.FaDonut/>} onClick={onMakeChocolateDonut}>
Focus the input
</Button>
</>
);
}
```
Essentially, useRef is like a “box” that can hold a mutable value in its `.current` property.
You might be familiar with refs primarily as a way to access the DOM. If you pass a ref object to React with ``<div ref={myRef} />``, React will set its .current property to the corresponding DOM node whenever that node changes.
## To what extent is an `Extent`...
:::info
When we make a link from a document, we don't necessarily always want to link from the entire document. For example if we are linking from a PVDonut's menu, we may want to link from just one particular donut - rather than the entire menu. For that, we need to create **extents**.
:::
:::success
**In this lab you will be implementing selecting an extent on both text and image nodes.** In order to create links, you need to know how to create the anchor that you are going to link from!
:::
### Extents in Images: `IImageExtent`
An extent is basically the terminology that we use to say we want to specify **this part** of a node. For an image node, that could be a selection of a particular person. For example we can see in the image below we have a selection over the two table tennis tables. **These are anchors, but within anchors the extent is what handles where exactly on the image the anchor should be!**

Given a list of anchors, you will be generating these visual indicators of where exactly the anchor is. **Note that if extent is null, then we are linking from the entire image**.
```typescript=
// Defines the extent of an anchor on an image node
export interface IImageExtent {
type: 'image'
left: number
top: number
width: number
height: number
}
```
:::warning
Since anchor.extent can be both an image or text type, we want to check that it is the correct type before we access the properties (eg. top, width) which will exist on IImageExtent but not ITextExtent. We can do that with the following condition:
`if (anchor.extent?.type == 'image')`
:::
:::success
**Task 3:** Load the existing randomly generated anchors onto your image using the `extent`!
**Note**: If selecting a region on an image does not work, then change the `divider` in the `onMove` method. This is annoyance of having screens with different resolutions!
:::
Here is an example of what we might want to add into the `anchorElementList` array. Feel free to edit this!
```
<div
id={anchor.anchorId}
key={'image.' + anchor.anchorId}
className="image-anchor"
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
style={{
height: anchor.extent.height,
left: anchor.extent.left,
top: anchor.extent.top,
width: anchor.extent.width,
}}
/>
```
Notice that we have a key property; this is because when we render the same property over again in our HTML DOM, it needs to have a unique key.
### Extents in Text: `ITextExtent`
First let us look at what an `ITextExtent` object looks like.
```typescript=
// Defines the extent of an anchor on a text node
export interface ITextExtent {
type: 'text'
startCharacter: number
endCharacter: number
text?: string // optional property to store text on extent
}
```
Imagine we have the following piece of text from PVDonuts.
> We’re not your typical donut shop. We’re a bit over the top.
We officially hit the scene when we <span style="color: blue; text-decoration: underline">opened our doors in 2016</span>, but we’ve been experimenting since 2014. We’re most popular for introducing brioche style donuts to the New England area, and they’re what makes us unique. We also offer old fashioned, <span style="color: blue; text-decoration: underline">filled brioche</span>, cake, crullers, fritters, and more. All of our signature styles are made, rolled, cut, dipped and decorated by hand each day.
As you can see, there are already existing links that are <span style="color: blue; text-decoration: underline">underlined and blue</span>. In our implementation, we would store this as an anchor with the extent defined as `ITextExtent`. This has a `startCharacter` and an `endCharacter` relative to paragraph. We want our anchors to have access to that information so that when we load our text we can visually show where our anchors are.
:::success
**Task 4:**
Go to `TextContent.tsx`, and manage the extent for when we want to create a new anchor - which would be when we click `Start Link`.
Here are the TODOs:
1. Add an onPointerUp method which updates the selected extent
3. Set the selected extent to null when this component loads
4. Update the textContent-wrapper HTML Element so that it works with useRef and onPointerUp
To check your implementation is correct, click `Start Link` and check that the alert that pops up is the expected `Extent` (Note: It should not be `undefined`)!
:::
### Extents on other document types
:::success
**Task 5**: Discuss these questions with a partner, and try to write out an interface for one of the following:
- What does an extent look like in a PDF? (Maybe we would want to combine image extents and text extents?)
- What would an extent look like in an audio recording?
- What would an extent look like for a video?
- What about a webpage?
:::
## Styling
Just a note that there are other ways to be `stylish` when writing HTML 😎 - feel free to use any you'd like!

### SCSS
This is what you have used so far - but there are many other ways to style - feel free to switch it up, or continue using `scss`. We just want to introduce you to some other ways of styling your web app!
:::success
If you decide to use an SCSS alternative, **you are responsible for converting all `.sass` files to the file format of your choice.**
:::
### Styling with Bootstrap
Sometimes, we want to be able to style our elements without creating an entire new `.scss` file. For many common styles like padding, margin, and [flex-box](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), you can just add a Bootstrap *utility class*.
We've added Bootstrap to our assignment repos, so the classes are ready to use! For padding and margin, there are defined levels (i.e. 1, 2, 3, 4, 5) that correspond to consistent values (i.e. 4px, 8px, 16px, 24px, 32px).
Having consistently scaling spacing values is generally good practice in design!
#### Padding
```htmlmixed=
// Padding level 3 on all sides
<div className="p-3">Hello!</div>
// Padding level 2 on the left
<div className="pl-2">Hello!</div>
// Padding level 4 on the right
<div className="pr-4">Hello!</div>
// Padding level 1 on the left and right
<div className="px-1">Hello!</div>
// Padding level 1 on the top and bottom
<div className="py-1">Hello!</div>
```
#### Margin
```htmlmixed=
// Margin level 3 on all sides
<div className="m-3">Hello!</div>
// Margin level 2 on the left
<div className="ml-2">Hello!</div>
// Margin level 4 on the right
<div className="mr-4">Hello!</div>
// Margin level 1 on the left and right
<div className="mx-1">Hello!</div>
// Margin level 1 on the top and bottom
<div className="my-1">Hello!</div>
```
#### Flexbox
Flexbox allows you to create responsive containers that nicely lay out your elements. You can create flexboxs that are horizontal (`flex-row`, the default) or vertical (`flex-column`).
For a great explanation and resource on flexbox, see this guide:
https://css-tricks.com/snippets/css/a-guide-to-flexbox/
Bootstrap flexbox classnames are pretty straightforward; here's some common ones:
```
d-flex (this means display: flex)
flex-row
flex-column
align-items-center
justify-content-center
```
# Checkoff
- Show a TA your `linkCollectionConnection.deleteLinks()`
- Show a TA that you have implemented text extent and image extent!
- Have the best day ever 😎
