Build A Simple Vue 3 App and Enjoy Astronomy! (Part 2 of 3)
Tags: beginners, vue, javascript
Published: 2022-01-13 20:21:47

## Project Debriefing

A picture is worth a thousand words. Here's what we're going to build today. 

[Astronomy of the Day Gallery](https://nasa-demo-devto.onrender.com/
)

If you haven't read [Part 1](https://dev.to/stoicllama/build-a-simple-vue-3-app-and-enjoy-astronomy-part-1-of-3-132a) yet, feel free to go back there first to get the starter template Vue app that we will continue to build upon for Part 2 and 3.

### _Table of Contents_
1. [Build the Components](#build) 
2. [Wire up the Components](#wire)

---

## <a name="build"></a> Build The Components

### 1. _public/index.html_

No change from the default Vue template app.

The code in index.html should look like the code below.

```javascript
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

```

### 2. _src/main.js_ 

Again no change here.

The code in main.js should look like this.

```javascript
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

createApp(App).use(router).mount("#app");
```

### 3. _src/views/Gallery.vue_

Delete the Home.vue and About.vue files, since we will not be using those views.

Create Gallery.vue under the src/views/ directory.

Gallery is the view that glues the APODCard.vue components together with the NASAServices.js that you will see in the next few steps.

Gallery code below.

```javascript
<template>
  <div class="gallery">
    <APODCard v-for="apod in APOD" :key="apod.url" :apod="apod" />
  </div>
</template>

<script>
// @ is an alias to /src
import APODCard from "@/components/APODCard.vue";
import NasaServices from "@/services/NasaServices.js";

export default {
  name: "Gallery",
  components: {
    APODCard,
  },
  data() {
    return {
      APOD: [],
    };
  },
  created() {
    NasaServices.getAPODList()
      .then((response) => {
        this.APOD = response.data;
      })
      .catch((error) => console.log(error));
  },
};
</script>

<style scoped>
.gallery {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
</style>

```

### 4. _src/components/APODCard.vue_

Delete the HelloWorld.vue file under the /src/components/ directory.

Create APODCard.vue file in the /src/components/ directory and paste the code below. 

```javascript
<template>
  <router-link
    :date="apod.date"
    :to="{ name: 'APODDetails', params: { date: apod.date } }"
  >
    <div class="card">
      <h2>{{ apod.title }}</h2>
      <img v-if="isImg()" :src="apod.url" :alt="apod.title" />
      <iframe v-else allowfullscreen :src="apod.url" :alt="apod.title"></iframe>
    </div>
  </router-link>
</template>

<script>
export default {
  name: "APODCard",
  props: {
    apod: {
      type: Object,
      required: true,
    },
  },
  methods: {
    isImg() {
      const regex = new RegExp("/image/");
      // console.log(this.apod.url);
      // console.log("regex.test(this.apod.url)" + regex.test(this.apod.url));
      if (!regex.test(this.apod.url)) {
        return false;
      }
      return true;
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
iframe {
  width: 20rem;
  height: 20rem;
}
img {
  width: 20rem;
  height: 20rem;
  object-fit: cover;
}
.card {
  padding: 20px;
  width: 20rem;
  cursor: pointer;
  border: 1px solid #39495c;
  margin-bottom: 18px;
}

.card:hover {
  transform: scale(1.01);
  box-shadow: 0 3px 12px 0 rgba(0, 0, 0, 0.2);
}

.card-link {
  color: #2c3e50;
  text-decoration: none;
}
</style>
```

There are three important items to note here related to the APODCard component.

First, each APOD card loaded in Gallery is an instance of the APODCard.vue component.

Second, there is an if-else condition in the template to render different HTML depending on whether the returned apod.url is an image or a video.

The APODCard component will call the function `isImg()` function to return a boolean value, namely, "Is this an image or a video?" Since the NASA API will sometimes return video versus an image, we need to use this boolean to determine the correct html to display the returned data from the NASA API. Specifically, we need to make sure if the data is a video, we need to embed it in an iframe to avoid a [cross origin reading block error](https://www.chromium.org/Home/chromium-security/corb-for-developers). 

```javascript
     <img v-if="isImg()" :src="apod.url" :alt="apod.title" />
     <iframe v-else allowfullscreen :src="apod.url" :alt="apod.title"></iframe>
```

Third, there is a regex logic embedded in the `isImg()` function to parse the URL to return the boolean of whether this url leads to an image or a video. If the apod.url has an "/image/" text in there, return true for image. Otherwise if "/image/" is not found in the apod.url, then return false to indicate video. Note we are using the standard [Regex library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) in the JavaScript library.

```javascript
isImg() {
      const regex = new RegExp("/image/");
      console.log(this.apod.url);
      console.log("regex.test(this.apod.url)" + regex.test(this.apod.url));
      if (!regex.test(this.apod.url)) {
        return false;
      }
      return true;
    },
```

### 5. _src/services/NasaServices.js_

Perform a quick install of the axios and luxon library in the terminal inside of the project folder.

```bash
npm i axios
npm i luxon
```

Create a new services folder, like so /src/services/.

In the services directory, create a file called NasaServices.js and paste the following code in there.

```javascript
import axios from "axios";
import { DateTime } from "luxon";

function getDate(offset) {
  let now = DateTime.now().minus({ days: offset });
  let dd = String(now.day).padStart(2, "0");
  let mm = String(now.month).padStart(2, "0");
  let yyyy = now.year;
  console.log("getDate(): " + `${yyyy}-${mm}-${dd}`);
  return `${yyyy}-${mm}-${dd}`;
}

let startDate = getDate(5);
let endDate = getDate(0);

export default {
  getAPOD(today) {
    return axios.get("https://api.nasa.gov/planetary/apod", {
      params: {
        api_key: process.env.VUE_APP_API_KEY,
        date: today,
      },
    });
  },
  getAPODList() {
    return axios.get("https://api.nasa.gov/planetary/apod", {
      params: {
        api_key: process.env.VUE_APP_API_KEY,
        start_date: startDate,
        end_date: endDate,
      },
    });
  },
};

```

Here we come upon the core logic of this Vue application. This is where we interface with the NASA API and get our raw data.

First of all, we use the [axios library](https://axios-http.com) for calling the NASA API with a GET request. In other words, we are sending a read-only request to the NASA API to _get_ raw data from their database.

To clarify, NasaServices.js is invoked from the Gallery view to get data. By itself, it will not get the data. It needs to be called from somewhere else to do so! :pen: _Hint:_ Can you find getAPODList() in Gallery.vue in the earlier steps? 

Second, the way the request is sent to the NASA API is through two parameters _startDate_ and _endDate_. The two parameters are calculated values from the getDate() function that gets the local date on the server that is hosting your website, or your local computer (if you are running it locally). We use the [luxon library](https://moment.github.io/luxon/#/?id=luxon) to help do the math to identify the date (mm-dd-yyyy) that is 5 days (startDate) from today (endDate). Otherwise, this Vue application would have to be much larger to accommodate all the edge cases with dates! (And we don't want to reinvent the wheel :smirk:.)

Third, the getAPOD() method will require an input for today's date. Here we use the apod.date (`this` refers to the apod object) as an input to find the details of that card. Note, in the Gallery view, the Vue application loops through the array of days retrieved from the NASA API (i.e., total 5 days); each day will get an APODCard component. Each APODCard component will in turn take the date of that day to provide to the getAPOD() method to get the details for that day to fill in the APODCard component display.

Fourth, the request also uses an API secret key to communicate with the NASA API. Remember the prerequisites in [Part 1](https://dev.to/stoicllama/build-a-simple-vue-3-app-and-enjoy-astronomy-part-1-of-3-132a) that ask you to get a personal NASA API key? If you want to get along quickly, you can use `DEMO_KEY` as the API key, but beware there is a limited number of times you can call it with the `DEMO_KEY`. 

:pen: _Tip:_ For security reasons the API key should be stored in a separate file `.env` under the root directory of your project with the format like so. 

```bash
VUE_APP_API_KEY = your_api_key_from_nasa
```

Make sure that `.env` is in the .gitignore file so you don't upload your secret key to Github by accident! It is simply adding '.env' in the file like so.

```bash
# local env files
.env
```

### 6. _src/views/APODDetails.vue_

Under the /src/views/ directory, create APODDetails.vue file, and paste the code below in the file.

This will be the details when the user clicks on one of the APODCard components in the Gallery view. They will be redirected by the Vue Router to come to the details for that specific APODCard date.

```javascript
<template>
  <div class="details__wrapper" v-if="apod">
    <div class="details">
      <h1>{{ apod.title }}</h1>
      <p>{{ apod.date }}</p>
      <img v-if="isImg()" :src="apod.url" :alt="apod.title" />
      <iframe v-else allowfullscreen :src="apod.url" :alt="apod.title"></iframe>
      <p>{{ apod.explanation }}</p>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import NasaServices from "@/services/NasaServices.js";

export default {
  name: "APODDetails",
  props: {
    date: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      apod: {},
    };
  },
  created() {
    NasaServices.getAPOD(this.date)
      .then((response) => {
        this.apod = response.data;
      })
      .catch((error) => console.log(error));
  },
  methods: {
    isImg() {
      const regex = new RegExp("/image/");
      console.log(this.apod.url);
      console.log("regex.test(this.apod.url)" + regex.test(this.apod.url));
      if (!regex.test(this.apod.url)) {
        return false;
      }
      return true;
    },
  },
};
</script>

<style scoped>
iframe {
  width: 30rem;
  height: 30rem;
}
img {
  width: 30rem;
  height: 30rem;
  object-fit: cover;
}
.details__wrapper {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.details {
  max-width: 40rem;
}
</style>
```

## <a name="wire"></a> Wire Up the Components

### 1. _src/App.vue_

Now it's time to wire up the components with the Vue application.

In src/App.vue, delete the original code, and paste the code below instead.

This tells the Vue application to load Gallery when the user does not type into any subdirectory for the url of the application.

```javascript
<template>
  <h1>Astronomy Photo of the Day (APOD)</h1>
  <div id="nav">
    <router-link to="/">Gallery</router-link>
  </div>
  <router-view />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>
```

### 2. _src/router/index.js_

Next, we can go to the router configuration.

Overlay the existing code with the code below in the src/router/index.js file. We're showing where the Gallery and the APODDetails views can be found by the Vue application to load into user browser.

```javascript
import { createRouter, createWebHistory } from "vue-router";
import Gallery from "../views/Gallery.vue";
import APODDetails from "@/views/APODDetails.vue";

const routes = [
  {
    path: "/",
    name: "Gallery",
    component: Gallery,
  },
  {
    path: "/apod/:date",
    name: "APODDetails",
    props: true,
    component: APODDetails,
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

```

Congratulations on making it this far! You can type `npm run serve` in your local terminal to see what the application looks like now on your computer (see video below). 

{% youtube mVeuaE0rliU %}

In the next article in Part 3, we will review on how to deploy this application to the interwebs so everyone can access it online!


---
## Article Series

Click on Part 3 to deploy the application on the internet!

[Build A Simple Vue 3 App and Enjoy Astronomy! (Part 1 of 3)](https://dev.to/stoicllama/build-a-simple-vue-3-app-and-enjoy-astronomy-part-1-of-3-132a)
Build A Simple Vue 3 App and Enjoy Astronomy! (Part 2 of 3)
[Build A Simple Vue 3 App and Enjoy Astronomy! (Part 3 of 3)](https://dev.to/stoicllama/build-a-simple-vue-3-app-and-enjoy-astronomy-part-3-of-3-1cc1)