How to Build a Vue.js Portfolio with Axios and Flickr API

Step 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.

By Tim Trott(Java|Type)Script / jQuery • May 7, 2019
2,729 words, estimated reading time 10 minutes.
How to Build a Vue.js Portfolio with Axios and Flickr API

We'll create a simple vue.js portfolio-style website in this tutorial series using Axios and the Flickr API. This series aims 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.

Building a Vue.js Website with Axios and Flickr API
Building a Vue.js Portfolio Website with Axios and Flickr API

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 can access the vue binary in your command line. You can verify that it is correctly 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 , then using your PowerShell entering the following:

vue create portfolio

The PowerShell window will show a wizard-style installer. Select the following options.

Take the default option of babel, eslint
Take the default option of babel, eslint
Vue create project can be slow. Be patient.
Vue create project can be slow. Be patient.

Once that is 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:

First Vue.js Project Welcome Screen
First Vue.js Project Welcome Screen

Now, in the PowerShell window, hit Ctrl + C to stop the server from 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 "API Keys" link, then "Create an App". Fill in the details; 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:

javascript
export default {
  api_key: "YOUR_KEY_HERE"
}

The vue.js portfolio app I will build will display the images contained within a Flickr album. Browse our Flickr albums and find the one you wish to display. In the URL bar, copy the album ID and add it to the config file.

Grab the Photoset ID from the URL bar of a Flickr Album
Grab the Photoset ID from the URL bar of a Flickr Album
javascript
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 Home.vue and replace it with this content.

xml
<template>
  <div class="home">
    
  </div>
</template>

<script>
export default {
  name: 'home',
};
</script>

Next, we will 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 complete listing at the end, which you can use to see how it fits together.

Firstly, we need some markup to show our page correctly.

xml
<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 must 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.

typescript
import config from '../../config';
import axios from 'axios';

Next, I'm going to define some data variables. One is to indicate if the page is busy (loading the contents), and the other variable is to hold the image data.

typescript
data() {
  return {
    loading: false,
    images: []
  }
},

Then, we add two functions: one that sets the page views up and another that fetches the images from Flickr.

typescript
  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 displayed. v-if can be used on any element to show or hide conditionally. Next, this method will call our other method with a JavaScript promise. If it is successful, the images will be put into our data container. If it fails, the error will be logged into the console.

The second method uses Axios to query the Flickr API. It's relatively straightforward. You can see where we get the photoset ID and API key from the config; it will call the flickr.photosets.getPhotos API method and return a few extra columns in the dataset.

The final thing we must do is get all this to show on the page load.

typescript
beforeMount(){
  this.loadImages()
}

Putting all that together, we get the full contents of Home.Vue.

typescript
<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>

Back in the PowerShell window, you can run the application using npm run serve. You should see an ugly screen showing JSON data if all goes well.

Ugly JSON output from Flickr API
Ugly JSON output from Flickr API

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

Vue components are tiny, self-contained modules that you can reuse in a project. Components can be reused as many times as you want, which is lucky since we will build one component and reuse it multiple times.

In your src folder, create a new folder called components. Create a new file within this folder called ImageCard.vue. Open this file in your editor.

The first thing we are going to add is the template markup.

xml
<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". 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. It's pretty simple here. We export a module named ImageCard with a property called image.

typescript
<script>
import config from '../../config';

export default {
  name: 'ImageCard',
  props: [ 'image' ]
}
</script>

Next to add some styling so it looks pretty. Putting it all together, we get this.

xml
<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>

We need to make a few changes to the Home to use this component in our app.vue file.

First, we need to import the component.

typescript
import ImageCard from '@/components/ImageCard';

Next, in the components array, we need to include ImageCard so that it is available to use.

typescript
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.

xml
<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 will repeat the component, passing each image to the component. Then we can add some styling, and you should have this as your Home.vue.

xml
<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.

Basic Vue Potfolio Website
Basic Vue Potfolio Website

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 to not break the page.

This is done by adding a computed section at the end of the class.

typescript
...
  beforeMount(){
    this.loadImages()
  },
  computed: {
    cleanImages() {
      return this.images.filter(image => image.url_n)
    }
  },
};

We need to adjust the for loop to look at the filtered items.

xml
<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.

typescript
export default {
  name: 'ImageCard',
  props: [ 'image' ],
  filters: {
    moment(date) {
      return moment(date).format("Do MMMM YYYY");
    }
  }
}

In the template, make a quick change to use this filter. This will pass the value of image.datetaken to the function moment and output the result.

xml
<p class="image-date">{{image.datetaken}}

Becomes

xml
<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.

xml
<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"> Add a catchy tagline or introduction here.


    </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.

typescript
import SiteHeader from '@/components/SiteHeader';
typescript
components: {
  SiteHeader,
  ImageCard
},
xml
<template>
  <div>
    <div class="wrapper" id="page">
      <site-header />
      <p v-if="loading" class="text-centered">

You should now have an excellent, pretty-looking portfolio website driven by a Flickr album using the Flickr API.

Building a Vue.js Website with Axios and Flickr API
Building a Vue.js Website with Axios and Flickr API

Conclusions and Download Sample Project

This has been a very quick introduction to Vue.js, including instructions on how to install Vue.js and 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 complete project source files using the link below.

Download from GitHub

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.

Related ArticlesThese articles may also be of interest to you

CommentsShare your thoughts in the comments below

My website and its content are free to use without the clutter of adverts, popups, marketing messages or anything else like that. If you enjoyed reading this article, or it helped you in some way, all I ask in return is you leave a comment below or share this page with your friends. Thank you.

There are no comments yet. Why not get the discussion started?

New comments for this post are currently closed.