How to Build a Vue.js Portfolio with Axios and Flickr APIStep by step guide to creating your first Vue.js portfolio app. We install and configure Vue.js, create a new project and build a portfolio website using Flickr API.
In this tutorial series, we'll create a simple vue.js portfolio style website using Axios, and the Flickr API. The goal of this series is to practice basic Vue concepts and gain exposure to working with an API. I am assuming a basic working knowledge of HTML, CSS, and Javascript.
The source files for this tutorial are available on GitHub at the end of the article.
Installing Vue.js CLI
Vue CLI requires Node.js version 8.9 or above. If you haven't got Node installed head over to the Node download page and follow the installation instructions to install Node.
To install the Vue.js package, enter the following command into PowerShell (or terminal) with administrator privileges.
npm install -g @vue/cli
After installation, you will have access to the vue
binary in your command line. You can verify that it is properly installed by simply running vue
, which should present you with a help message listing all available commands.
Vue.js Portfolio Project Setup
Now we get to create our Vue.js portfolio project. Navigate to where you want to save (e.g. c:\dev\
) it in your PowerShell and entering the following:
vue create portfolio
The PowerShell window will show a wizard-style installer. Simply select the following options.
Once that it done it will create a folder called portfolio
in c:\dev\
(or wherever you ran the command) and create a sample template.
CD into the new portfolio
folder and run this command to confirm the project was successfully created.
npm run dev
If everything has worked successfully you will see the following in your browser:
Now in the PowerShell window, hit Ctrl+C to stop the server running. We need to install a few packages to get our app running.
First, we will use axios to handle our AJAX requests to the Flickr API. Install the package by running this command in the PowerShell window.
npm install axios --save
Next, we are going to use the moment package to format dates for us. Install moment
using this command.
npm install moment --save
If all goes well you should see a successful message.
Flickr API Setup
Working with the Flickr API requires a key. This key is a unique identifier that lets Flickr know we're a legitimate source making a request. The key is free but you do have to have a Flickr account.
Head over to Flickr and log in (register for an account if you don't have one) then visit the Flick API Services page. Take a moment to read the guides under "Read these first" especially the terms and conditions , so you don't get banned.
Click the link for "API Keys" then "Create an App". Fill in the details and once validated you will get a Key and a secret.
In the root of the project directory, (at the same level that index.html is located) create a file called config.js
and add the following contents:
export default {
api_key: "YOUR_KEY_HERE"
}
The vue.js portfolio app I am going to build will display the images contained within a Flickr album. Simply browse our Flickr albums and find the one you wish to display. In the URL bar copy out the album ID and add it to the config file.
export default {
api_key: "YOUR_KEY_HERE",
photoset: "72157700194273182"
}
Making Our First API Call
We now have all the pieces to fetch photos from Flickr. Empty out Home.vue
and replace it with this contents.
<template>
<div class="home">
</div>
</template>
<script>
export default {
name: 'home',
};
</script>
Next, we are going to add some code to call the Flickr API using our key and show the results on the page. I'll explain each chunk of code and provide a full listing at the end which you can use to see how it fits together.
Firstly we need some markup to show our page properly.
<template>
<div>
<div class="wrapper" id="page">
<p v-if="loading">
Loading...
<ul v-else>
<li v-for="image in images" :key="image.id">{{image}}</li>
</ul>
</div>
</div>
</template>
Next, we need to import our config file and the axios
package into the vue. This is done using the import
command followed by the module being imported and then the file it is imported from.
import config from '../../config';
import axios from 'axios';
Next, I'm going to define some data variables. One to indicate if the page is busy (loading the contents) and another variable to hold the image data.
data() {
return {
loading: false,
images: []
}
},
Then we add two functions - one that sets the page views up and another which fetches the images from Flickr.
methods: {
loadImages() {
this.loading = true;
this.fetchImages()
.then((response) => {
this.images = response.data.photoset.photo;
this.loading = false;
})
.catch((error) => {
console.log("An error ocurred: ", error);
})
},
fetchImages() {
return axios({
method: 'get',
url: 'https://api.flickr.com/services/rest',
params: {
method: 'flickr.photosets.getPhotos',
api_key: config.api_key,
photoset_id: config.photoset,
extras: 'url_n, url_o, owner_name, date_taken, views',
page: 1,
format: 'json',
nojsoncallback: 1,
per_page: 30,
}
})
},
},
The first method, loadImages
, is going to set the loading
variable to true. Through the magic of Vue data binding when the loading
variable is true the v-if
statement becomes true and the loading text is shown, else the list will be shown. v-if
can be used on any element to conditionally show or hide. Next, this method is going to call our other method with a JavaScript promise. If it is successful it will put the images into our data container. If it fails the error will be logged in the console.
The second method uses Axios to query the Flickr API. It's fairly straightforward. You can see where we get the photoset id and API key from the config, it's going to call the flickr.photosets.getPhotos
API method and return a few extra columns in the dataset.
The final thing we need to do is to get all this to show on the page load.
beforeMount(){
this.loadImages()
}
Putting all that together we get the full contents of Home.Vue
.
<template>
<div>
<div class="wrapper" id="page">
<site-header />
<p v-if="loading">
Loading...
<ul v-else>
<li v-for="image in images" :key="image.id">{{image}}</li>
</ul>
</div>
</div>
</template>
<script>
import config from '../../config';
import axios from 'axios';
export default {
name: 'home',
components: { },
data() {
return {
loading: false,
images: []
}
},
methods: {
loadImages() {
this.loading = true;
this.fetchImages()
.then((response) => {
this.images = response.data.photoset.photo;
this.loading = false;
})
.catch((error) => {
console.log("An error ocurred: ", error);
})
},
fetchImages() {
return axios({
method: 'get',
url: 'https:/api.flickr.com/services/rest',
params: {
method: 'flickr.photosets.getPhotos',
api_key: config.api_key,
photoset_id: config.photoset,
extras: 'url_n, url_o, owner_name, date_taken, views',
page: 1,
format: 'json',
nojsoncallback: 1,
per_page: 30,
}
})
},
},
beforeMount(){
this.loadImages()
}
};
</script>
Now back in the PowerShell window, you can run the application using npm run serve
. If all goes well you should see an ugly screen showing a bunch of JSON data.
The next thing to do is tidy this up and create a component or two to display the image nicely.
Make a Vue.js Component
Components in Vue are tiny little self-contained modules which you can reuse in a project. Components can be reused as many times as you want, which is lucky since we are going to build one component and reuse it multiple times.
In your src
folder, create a new folder called components
. Within this folder create a new file called ImageCard.vue
. Open this file in your editor.
The first thing we are going to add is the template markup.
<template>
<div class="item">
<a href="#">
<img class="image" :src="image.url_n" :alt="image.title">
<div class="body">
<p v-if="image.title" class="image-title">{{image.title}}
<p v-else class="image-title">No Title Found
<p class="image-owner">By {{image.ownername}}
<section class="image-date-view-wrapper">
<p class="image-date">{{image.datetaken}}
<p class="image-views">Views: {{image.views}}
</section>
</div>
</a>
</div>
</template>
You may have noticed the colon before an attribute, for example, I used :alt="image.title"
. This just means that the attribute is data bound to the image object's title property. For outputting data as plain text, we surround it in handlebars - {{image.title}}
.
Next, we add the TypeScript code. Fairly simple here. We export a module with the name ImageCard
and it has a property called image.
<script>
import config from '../../config';
export default {
name: 'ImageCard',
props: [ 'image' ]
}
</script>
And finally, some styling so it looks pretty. Putting it all together we get this.
<template>
<div class="item">
<a href="#">
<img class="image" :src="image.url_n" :alt="image.title">
<div class="body">
<p v-if="image.title" class="image-title">{{image.title}}
<p v-else class="image-title">No Title Found
<p class="image-owner">By {{image.ownername}}
<section class="image-date-view-wrapper">
<p class="image-date">{{image.datetaken }}
<p class="image-views">Views: {{image.views}}
</section>
</div>
</a>
</div>
</template>
<script>
import config from '../../config';
export default {
name: 'ImageCard',
props: [ 'image' ]
}
</script>
<style lang="scss">
.item {
background-color: #eee;
display: inline-block;
margin: 0 0 1em;
width: 100%;
}
.item a {text-decoration:none}
.item:hover .body {
visibility: visible;
opacity: 1;
}
.image {
width: 100%;
height: auto;
object-fit: cover;
}
.body {
padding: .5rem 1rem 1rem;
position:relative;
height:95px;
margin:-100px 0 0 0;
background:rgba(0,0,0,0.5);
color:white;
visibility: hidden;
opacity: 0;
transition: visibility 0s, opacity 0.5s linear;
}
.image-title {
font-weight: bold;
margin: 0;
}
.image-owner {
margin-top: 0;
font-size: .8rem;
}
.image-title,
.image-owner {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.image-date-view-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
}
.image-date,
.image-views {
margin-bottom: 0;
font-size: .8rem;
}
</style>
Now to use this component in our app we need to make a few changes to the Home.vue file.
First, we need to import the component.
import ImageCard from '@/components/ImageCard';
Next, in the components array, we need to include ImageCard so that it is available to use.
components: { ImageCard },
Next, we change the template markup to make use of the component. Instead of a list of raw image data, change it to this.
<p v-if="loading" class="text-centered">
Loading...
<div v-else class="masonry">
<image-card v-for="image in images" :key="image.id" :image="image" />
</div>
The property :image
we created in the component will be data-bound and contain the details from the image item in the loop. The loop is going to repeat the component passing in each image to the component in turn. Finally, we add some styling and you should have this as your Home.vue
.
<template>
<div>
<div class="wrapper" id="page">
<p v-if="loading" class="text-centered">
Loading...
<div v-else class="masonry">
<image-card v-for="image in images" :key="image.id" :image="image" />
</div>
</div>
</div>
</template>
<script>
import config from '../../config';
import axios from 'axios';
import ImageCard from '@/components/ImageCard';
export default {
name: 'home',
components: {
ImageCard
},
data() {
return {
loading: false,
images: []
}
},
methods: {
loadImages() {
this.loading = true;
this.fetchImages()
.then((response) => {
this.images = response.data.photoset.photo;
this.loading = false;
})
.catch((error) => {
console.log("An error ocurred: ", error);
})
},
fetchImages() {
return axios({
method: 'get',
url: 'https://api.flickr.com/services/rest',
params: {
method: 'flickr.photosets.getPhotos',
api_key: config.api_key,
photoset_id: config.photoset,
extras: 'url_n, url_o, owner_name, date_taken, views',
page: 1,
format: 'json',
nojsoncallback: 1,
per_page: 30,
}
})
},
},
beforeMount(){
this.loadImages()
}
};
</script>
<style lang="scss">
.text-centered {
text-align: center;
}
.wrapper {
margin: 0 auto;
max-width: 1200px;
@media only screen and (max-width: 799px) {
max-width: 100%;
margin: 0 1.5rem;
}
}
.masonry {
margin: 1.5em auto;
max-width: 1200px;
column-gap: 1.5em;
}
@media only screen and (min-width: 1024px) {
.masonry {
column-count: 3;
}
}
/* Masonry on medium-sized screens */
@media only screen and (max-width: 1023px) and (min-width: 768px) {
.masonry {
column-count: 2;
}
}
/* Masonry on small screens */
@media only screen and (max-width: 767px) and (min-width: 540px) {
.masonry {
column-count: 1;
}
}
</style>
You should now have something like this in your browser. Your images will vary depending on the photoset you use.
Tiding Up Our App
There are a few things we can do to tidy up the app.
Sometimes broken images come through from the API, so we can easily filter them out so as not to break the page.
This is done by adding a computed section at the end of the class.
...
beforeMount(){
this.loadImages()
},
computed: {
cleanImages() {
return this.images.filter(image => image.url_n)
}
},
};
We then just need to adjust the for loop to look at the filtered items.
<image-card v-for="image in cleanImages" :key="image.id" :image="image" />
Next in the ImageCard.vue
we can tidy up the dates using the moment library. This is done by creating a filter which will return formatted values.
export default {
name: 'ImageCard',
props: [ 'image' ],
filters: {
moment(date) {
return moment(date).format("Do MMMM YYYY");
}
}
}
In the template simply make a quick change to use this filter. This will pass the value of image.datetaken
to the function moment
and output the result.
<p class="image-date">{{image.datetaken}}
Becomes
<p class="image-date">{{image.datetaken | moment}}
I also added a header to the page by creating a new component called SiteHeader.vue
with the following contents.
<template>
<div>
<header>
<a href="#"><h1>My Photography Portfolio</h1></a>
<div class="menu">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Twitter</a></li>
</ul>
</div>
</header>
<h2 class="text-centered">Hi. I'm a Photographer</h2>
<p class="text-centered">You can add a catchy tagline or introduction here if you wish.
</div>
</template>
<script>
export default {
name: 'SiteHeader'
}
</script>
<style lang="scss">
header {
margin:1em 0 0 0;
}
header::after{
content: "";
clear: both;
display: table;
}
h1 {
width: 190px;
height: 72px;
background: url("../assets/logo.png") top right;
text-indent: -9999px;
margin: 0 auto;
}
.menu ul {
margin:1em 0 0 0;
padding:0;
}
.menu li {
width:100%;
padding:0.25em;
display:block;
list-style:none;
margin:0;
text-align:center;
}
.menu li a {
color:#777;
text-decoration:none;
text-transform:uppercase;
}
h2 {
margin:2em auto 1em auto;
}
p.text-centered {
color:#777;
margin-bottom:3em;
}
@media only screen and (min-width: 1024px) {
h1 {
margin:0;
float:left;
}
.menu {
float:right;
}
.menu li {
display:inline;
}
}
</style>
This is included in the Home.vue by adding an imports and markup tag.
import SiteHeader from '@/components/SiteHeader';
components: { SiteHeader, ImageCard },
<template>
<div>
<div class="wrapper" id="page">
<site-header />
<p v-if="loading" class="text-centered">
You should now have a nice pretty looking portfolio website driven from a Flickr album using the Flickr API.
Conclusions and Download Sample Project
This has been a very quick introduction to Vue.js, how to install Vue.js, create a new project, using Axios to query the Flick API. We've seen how to build a component and use filters over a data set.
You can download the full project source files using the link below.
Did you find this tutorial helpful? Was there anything missing or something you got stuck on? Let me know in the comments below and I'll do my best to fill in the gaps.