Project Debriefing
A picture is worth a thousand words. Here's what we're going to build today.
If you haven't read Part 1 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
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.
<!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.
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.
<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.
<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.
<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 in the JavaScript library.
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.
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.
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 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! 🖊️ 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 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 😏.)
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 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
.
🖊️ 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.
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.
# 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.
<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>
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.
<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.
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).
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)
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)
Top comments (1)
Helpful step-by-steps!