How To Create A GitHub User Search Web App Using Vue.js

GitHub User Search Web App Main Logo

How To Create A GitHub User Search Web App Using Vue.js

In this short article, we are going to show you how you can use a Vue.js to create a simple GitHub user finder. Because we think that everyone already knows how to write on React, Svelte, Angular framework/libraries.

Well, how can I do without Vue? It’s time to fill this gap.

GitHub User Search Web App Photo 1

So, today we will create the same application using Vue, write tests for it on Cypress and slightly affect Vue CLI 3.

Preparation

First, install the latest Vue CLI:


npm i -g @vue/cli

And run the creation of the project:


vue create vue-github-search

We follow the steps of the generator. For our project, I chose a Manual mode and the following configuration:

GitHub User Search Web App Photo 2

Additional modules

As styles, we’ll use Stylus, so we’ll need a stylus and stylus-loader. We also need Axios for requests to the network and Lodash, from which we will take the debounce function.

Let’s move to the project folder and install the necessary packages:


cd vue-github-search
npm i stylus stylus-loader axios lodash

Checking

Run the project and make sure that everything works:


npm run serve

All changes in the code will be instantly applied in the browser without reloading the page.

Store

To begin with, let’s write a vuex store where all the application data will be. We need nothing more to store: a search query, user data, and a boot process flag.
Open store.js and describe the initial state of the application and the necessary mutations:


...
const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
const SET_LOADING = 'SET_LOADING';
const SET_USER = 'SET_USER';
const RESET_USER = 'RESET_USER';

export default new Vuex.Store({
state: {
searchQuery: '',
loading: false,
user: null
},
mutations: {
[SET_SEARCH_QUERY]: (state, searchQuery) => state.searchQuery = searchQuery,
[SET_LOADING]: (state, loading) => state.loading = loading,
[SET_USER]: (state, user) => state.user = user,
[RESET_USER]: state => state.user = null
}
});

We add an action for uploading data from the GitHub API and to change the search query (we need it for the search line). As a result, our store will look like this:


import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
const SET_LOADING = 'SET_LOADING';
const SET_USER = 'SET_USER';
const RESET_USER = 'RESET_USER';

export default new Vuex.Store({
state: {
searchQuery: '',
loading: false,
user: null
},
mutations: {
[SET_SEARCH_QUERY]: (state, searchQuery) => state.searchQuery = searchQuery,
[SET_LOADING]: (state, loading) => state.loading = loading,
[SET_USER]: (state, user) => state.user = user,
[RESET_USER]: state => state.user = null
},
actions: {
setSearchQuery({commit}, searchQuery) {
commit(SET_SEARCH_QUERY, searchQuery);
},
async search({commit, state}) {
commit(SET_LOADING, true);
try {
const {data} = await axios.get(`https://api.github.com/users/${state.searchQuery}`);
commit(SET_USER, data);
} catch (e) {
commit(RESET_USER);
}
commit(SET_LOADING, false);
}
}
});

Search line

Create a new Search.vue component in the components folder. Add a computed property to associate the component with the store. When changing the search query, we will call the search with debouncing.


<template>
<input v-model="query" @input="debouncedSearch" placeholder="Enter username" />
</template>

<script>
import {mapActions, mapState} from 'vuex';
import debounce from 'lodash/debounce';

export default {
name: 'search',
computed: {
...mapState(['searchQuery']),
query: {
get() {
return this.searchQuery;
},
set(val) {
return this.setSearchQuery(val);
}
}
},
methods: {
...mapActions(['setSearchQuery', 'search']),
debouncedSearch: debounce(function () {
this.search();
}, 500)
}
};
</script>

<style lang="stylus" scoped>
input
width 100%
font-size 16px
text-align center
</style>

Now we connect our search line to the main component of App.vue and simultaneously delete the extra lines created by the generator.


<template>
<div id="app">
<Search />
</div>
</template>

<script>
import Search from './components/Search';

export default {
name: 'app',
components: {
Search
}
};
</script>

<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
font-smoothing antialiased
margin 10px
</style>

Look at the result in the browser, making sure that everything works with vue-devtools:

GitHub User Search Web App Photo 3

As you can see, we already have all the logic of the application! We enter the username, the query is executed, and the profile data is stored in the store.

User Profile

Create a User.vue component and add logic to indicate the load, display the profile and an error when the user is not found. Also, we will add an animation of transitions.


<template>
<div class="github-card">
<transition name="fade" mode="out-in">
<div v-if="loading" key="loading">
Loading
</div>
<div v-else-if="user" key="user">
<div class="background" :style="{backgroundImage: `url(${user.avatar_url})`}" />
<div class="content">
<a class="avatar" :href="`https://github.com/${user.login}`" target="_blank">
<img :src="user.avatar_url" :alt="user.login" />
</a>
<h1>{{user.name || user.login}}</h1>
<ul class="status">
<li>
<a :href="`https://github.com/${user.login}?tab=repositories`" target="_blank">
<strong>{{user.public_repos}}</strong>
<span>Repos</span>
</a>
</li>
<li>
<a :href="`https://gist.github.com/${user.login}`" target="_blank">
<strong>{{user.public_gists}}</strong>
<span>Gists</span>
</a>
</li>
<li>
<a :href="`https://github.com/${user.login}/followers`" target="_blank">
<strong>{{user.followers}}</strong>
<span>Followers</span>
</a>
</li>
</ul>
</div>
</div>
<div v-else key="not-found">
User not found
</div>
</transition>
</div>
</template>

<script>
import {mapState} from 'vuex';

export default {
name: 'User',
computed: mapState(['loading', 'user'])
};
</script>

<style lang="stylus" scoped>
.github-card
margin-top 50px
padding 20px
text-align center
background #fff
color #000
position relative
h1
margin 16px 0 20px
line-height 1
font-size 24px
font-weight 500
.background
filter blur(10px) opacity(50%)
z-index 1
position absolute
top 0
left 0
right 0
bottom 0
background-size cover
background-position center
background-color #fff
.content
position relative
z-index 2
.avatar
display inline-block
overflow hidden
background #fff
border-radius 100%
text-decoration none
img
display block
width 80px
height 80px
.status
background white
ul
text-transform uppercase
font-size 12px
color gray
list-style-type none
margin 0
padding 0
border-top 1px solid lightgray
border-bottom 1px solid lightgray
zoom 1
&:after
display block
content ''
clear both
li
width 33%
float left
padding 8px 0
box-shadow 1px 0 0 #eee
&:last-of-type
box-shadow none
strong
display block
color #292f33
font-size 16px
line-height 1.6
a
color #707070
text-decoration none
&:hover
color #4183c4

.fade-enter-active, .fade-leave-active
transition opacity .5s
.fade-enter, .fade-leave-to
opacity 0
</style>

Connect our component in App.vue and enjoy the result:


<template>
<div id="app">
<Search />
<User />
</div>
</template>

<script>
import Search from './components/Search';
import User from './components/User';

export default {
name: 'app',
components: {
User,
Search
}
};
</script>

<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
font-smoothing antialiased
margin 10px
</style>

GitHub User Search Web App Photo 4

Tests

Write simple tests for our application.


describe('Github User Search', () => {
it('has input for username', () => {
cy.visit('/');
cy.get('input');
});
it('has "User not found" caption', () => {
cy.visit('/');
cy.contains('User not found');
});
it("finds Linus Torvalds' GitHub page", () => {
cy.visit('/');
cy.get('input').type('torvalds');
cy.contains('Linus Torvalds');
cy.get('img');
cy.contains('span', 'Repos');
cy.contains('span', 'Gists');
cy.contains('span', 'Followers');
});
it("doesn't find nonexistent page", () => {
cy.visit('/');
cy.get('input').type('_some_random_name_6m92msz23_2');
cy.contains('User not found');
});
});

Run the tests command


npm run test:e2e

In the opened window, click the button Run all specs and see that the tests pass:

GitHub User Search Web App Photo 5

Assembly

Vue CLI 3 supports a new mode of assembly of the application, modern mode. It creates 2 versions of scripts: lightweight for modern browsers that support the latest JavaScript features, and a full version with all the necessary polyphyly for older ones. The main advantage is that we absolutely do not need to bother with the release of such an application. It just works. If the browser supports <script type = “module”>, it will itself lighten the lightweight build. How it works, you can read more in this article.

Add the modern flag to the build command in package.json:


"build": "vue-cli-service build --modern"

We are collecting the project:


npm run build

Let’s look at the size of the resulting scripts:


8.0K ./app-legacy.cb7436d4.js
8.0K ./app.b16ff4f7.js
116K ./chunk-vendors-legacy.1f6dfb2a.js
96K ./chunk-vendors.a98036c9.js

As you can see, the new method really reduces the size of the assembly. The difference will be even more noticeable on large projects, so the feature definitely deserves attention.

References/code

GitHub repository