From 7d522583780cae5746112f204b3f8c604d5a87bd Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Tue, 7 Mar 2017 21:38:55 +0100
Subject: [PATCH 01/12] Move rejection handler

---
 src/services/timeline_fetcher/timeline_fetcher.service.js | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 40f568c3..e684a170 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -5,6 +5,8 @@ import apiService from '../api/api.service.js'
 const update = ({store, statuses, timeline, showImmediately}) => {
   const ccTimeline = camelCase(timeline)
 
+  setError({store, timeline, value: false})
+
   store.dispatch('addNewStatuses', {
     timeline: ccTimeline,
     statuses,
@@ -33,9 +35,8 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
   }
 
   return apiService.fetchTimeline(args)
-    .then((statuses) => update({store, statuses, timeline, showImmediately}))
-    .then(() => setError({store, timeline, value: false}))
-    .catch(() => setError({store, timeline, value: true}))
+    .then((statuses) => update({store, statuses, timeline, showImmediately}),
+      () => setError({store, timeline, value: true}))
 }
 
 const startFetching = ({ timeline = 'friends', credentials, store }) => {

From a6b6fe95c0fe2aa60ebbfca87fde47e629035c49 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 18:28:41 +0100
Subject: [PATCH 02/12] Show visual feedback on login error, redirect on
 success

---
 src/components/login_form/login_form.js  |  8 ++-
 src/components/login_form/login_form.vue |  9 ++++
 src/modules/users.js                     | 68 +++++++++++++-----------
 3 files changed, 53 insertions(+), 32 deletions(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 827c704c..2ad5b0b5 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -1,13 +1,17 @@
 const LoginForm = {
   data: () => ({
-    user: {}
+    user: {},
+    authError: false
   }),
   computed: {
     loggingIn () { return this.$store.state.users.loggingIn }
   },
   methods: {
     submit () {
-      this.$store.dispatch('loginUser', this.user)
+      this.$store.dispatch('loginUser', this.user).then(
+        () => { this.$router.push('/main/friends')}, 
+        () => { this.authError = true }
+      )
     }
   }
 }
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index c0273bae..279469ee 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -17,6 +17,9 @@
         <div class='form-group'>
           <button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button>
         </div>
+        <div v-if="authError" class='form-group'>
+          <button disabled='true' class='btn btn-default base05 error'>Error logging in, try again</button>
+        </div>
       </form>
     </div>
   </div>
@@ -39,6 +42,12 @@
     margin-top: 1.0em;
     min-height: 28px;
   }
+
+  .error {
+    margin-top: 0em;
+    margin-bottom: 0em;
+    background-color: rgba(255, 48, 16, 0.65);
+  }
 }
 
 </style>
diff --git a/src/modules/users.js b/src/modules/users.js
index 31731880..a5274480 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -65,40 +65,48 @@ const users = {
       })
     },
     loginUser (store, userCredentials) {
-      const commit = store.commit
-      commit('beginLogin')
-      store.rootState.api.backendInteractor.verifyCredentials(userCredentials)
-        .then((response) => {
-          if (response.ok) {
-            response.json()
-              .then((user) => {
-                user.credentials = userCredentials
-                commit('setCurrentUser', user)
-                commit('addNewUsers', [user])
+      return new Promise((resolve, reject) => {
+        const commit = store.commit
+        commit('beginLogin')
+        store.rootState.api.backendInteractor.verifyCredentials(userCredentials)
+          .then((response) => {
+            if (response.ok) {
+              response.json()
+                .then((user) => {
+                  user.credentials = userCredentials
+                  commit('setCurrentUser', user)
+                  commit('addNewUsers', [user])
 
-                // Set our new backend interactor
-                commit('setBackendInteractor', backendInteractorService(userCredentials))
+                  // Set our new backend interactor
+                  commit('setBackendInteractor', backendInteractorService(userCredentials))
 
-                // Start getting fresh tweets.
-                store.dispatch('startFetching', 'friends')
+                  // Start getting fresh tweets.
+                  store.dispatch('startFetching', 'friends')
 
-                // Get user mutes and follower info
-                store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
-                  each(mutedUsers, (user) => { user.muted = true })
-                  store.commit('addNewUsers', mutedUsers)
+                  // Get user mutes and follower info
+                  store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
+                    each(mutedUsers, (user) => { user.muted = true })
+                    store.commit('addNewUsers', mutedUsers)
+                  })
+
+                  // Fetch our friends
+                  store.rootState.api.backendInteractor.fetchFriends()
+                    .then((friends) => commit('addNewUsers', friends))
                 })
-
-                // Fetch our friends
-                store.rootState.api.backendInteractor.fetchFriends()
-                  .then((friends) => commit('addNewUsers', friends))
-              })
-          }
-          commit('endLogin')
-        })
-        .catch((error) => {
-          console.log(error)
-          commit('endLogin')
-        })
+            } else {
+              // Authentication failed
+              commit('endLogin')
+              reject()
+            }
+            commit('endLogin')
+            resolve()
+          })
+          .catch((error) => {
+            console.log(error)
+            commit('endLogin')
+            reject()
+          })
+      })
     }
   }
 }

From c0e8111d642ca9f85fbb4091f2ac9e86f4238a58 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 19:08:01 +0100
Subject: [PATCH 03/12] Clear username and password field on failed login

---
 src/components/login_form/login_form.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 2ad5b0b5..e489f381 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -10,7 +10,11 @@ const LoginForm = {
     submit () {
       this.$store.dispatch('loginUser', this.user).then(
         () => { this.$router.push('/main/friends')}, 
-        () => { this.authError = true }
+        () => {
+          this.authError = true
+          this.user.username = ''
+          this.user.password = ''
+        }
       )
     }
   }

From ccc460bb5ed1c8b7338f8a26bdb3029c74b26024 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 19:22:56 +0100
Subject: [PATCH 04/12] Give more specific reason for failed login

---
 src/components/login_form/login_form.js  | 4 ++--
 src/components/login_form/login_form.vue | 2 +-
 src/modules/users.js                     | 8 ++++++--
 3 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index e489f381..bc801397 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -10,8 +10,8 @@ const LoginForm = {
     submit () {
       this.$store.dispatch('loginUser', this.user).then(
         () => { this.$router.push('/main/friends')}, 
-        () => {
-          this.authError = true
+        (error) => {
+          this.authError = error
           this.user.username = ''
           this.user.password = ''
         }
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 279469ee..8a32e064 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -18,7 +18,7 @@
           <button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button>
         </div>
         <div v-if="authError" class='form-group'>
-          <button disabled='true' class='btn btn-default base05 error'>Error logging in, try again</button>
+          <button disabled='true' class='btn btn-default base05 error'>{{authError}}</button>
         </div>
       </form>
     </div>
diff --git a/src/modules/users.js b/src/modules/users.js
index a5274480..482c3b14 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -96,7 +96,11 @@ const users = {
             } else {
               // Authentication failed
               commit('endLogin')
-              reject()
+              if (response.status === 401) {
+                reject('Wrong username or password')
+              } else {
+                reject('An error occured, please try again')
+              }
             }
             commit('endLogin')
             resolve()
@@ -104,7 +108,7 @@ const users = {
           .catch((error) => {
             console.log(error)
             commit('endLogin')
-            reject()
+            reject('Failed to connect to server, try again')
           })
       })
     }

From 0810b2d51a6f0fbbfe1604f6d1954cde8ed08290 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 19:31:39 +0100
Subject: [PATCH 05/12] Fix typo

---
 src/modules/users.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index 482c3b14..51643bd1 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -99,7 +99,7 @@ const users = {
               if (response.status === 401) {
                 reject('Wrong username or password')
               } else {
-                reject('An error occured, please try again')
+                reject('An error occurred, please try again')
               }
             }
             commit('endLogin')

From 9511691c9467e26c3237b4a2c936e8a757b3e515 Mon Sep 17 00:00:00 2001
From: shpuld <shpuld@gmail.com>
Date: Thu, 9 Mar 2017 02:21:23 +0200
Subject: [PATCH 06/12] Make the error into a div instead of a button to get
 rid of the hover effects.

---
 src/components/login_form/login_form.vue | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 8a32e064..b2fa5341 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -18,7 +18,7 @@
           <button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button>
         </div>
         <div v-if="authError" class='form-group'>
-          <button disabled='true' class='btn btn-default base05 error'>{{authError}}</button>
+          <div class='error base05'>{{authError}}</div>
         </div>
       </form>
     </div>
@@ -44,9 +44,11 @@
   }
 
   .error {
-    margin-top: 0em;
-    margin-bottom: 0em;
+    border-radius: 5px;
+    text-align: center;
     background-color: rgba(255, 48, 16, 0.65);
+    min-height: 28px;
+    line-height: 28px;
   }
 }
 

From 502757da28d573641a48197c284b7e40dfc8154e Mon Sep 17 00:00:00 2001
From: xj9 <xj9@heropunch.io>
Date: Wed, 8 Mar 2017 20:23:10 -0700
Subject: [PATCH 07/12] improvements on fature/better-nsfw-image-loading

- loading indicator
- avoid hitting the cache if we already know the image was loaded
- more responsive toggle
---
 src/components/attachment/attachment.js  | 24 +++++++++++++++++++-----
 src/components/attachment/attachment.vue | 18 ++++++++++--------
 2 files changed, 29 insertions(+), 13 deletions(-)

diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index c3f52f57..7715add5 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -11,7 +11,9 @@ const Attachment = {
     return {
       nsfwImage,
       hideNsfwLocal: this.$store.state.config.hideNsfw,
-      showHidden: false
+      showHidden: false,
+      loading: false,
+      img: document.createElement('img')
     }
   },
   computed: {
@@ -20,6 +22,13 @@ const Attachment = {
     },
     hidden () {
       return this.nsfw && this.hideNsfwLocal && !this.showHidden
+    },
+    autoHeight () {
+      if (this.type === 'image' && this.nsfw) {
+        return {
+          'min-height': '311px'
+        }
+      }
     }
   },
   methods: {
@@ -29,10 +38,15 @@ const Attachment = {
       }
     },
     toggleHidden () {
-      let img = document.createElement('img')
-      img.src = this.attachment.url
-      img.onload = () => {
-        this.showHidden = !this.showHidden
+      if (this.img.onload) {
+        this.img.onload()
+      } else {
+        this.loading = true
+        this.img.src = this.attachment.url
+        this.img.onload = () => {
+          this.loading = false
+          this.showHidden = !this.showHidden
+        }
       }
     }
   }
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index ad60acf9..8f51b891 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -1,15 +1,14 @@
 <template>
-  <div class="attachment" :class="type">
-    <a class="image-attachment" v-if="hidden" v-on:click.prevent="toggleHidden()">
-      <img :key="nsfwImage" :src="nsfwImage"></img>
+  <div class="attachment" :class="{[type]: true, loading}" :style="autoHeight">
+    <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
+      <img :key="nsfwImage" :src="nsfwImage"/>
     </a>
     <div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
       <a href="#" @click.prevent="toggleHidden()">Hide</a>
     </div>
 
-    <a class="image-attachment" v-if="type === 'image' && !hidden"
-      :href="attachment.url" target="_blank">
-      <img class="base05-border" referrerpolicy="no-referrer" :src="attachment.large_thumb_url || attachment.url"></img>
+    <a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank">
+      <img class="base05-border" referrerpolicy="no-referrer" :src="attachment.large_thumb_url || attachment.url"/>
     </a>
 
     <video v-if="type === 'video' && !hidden" :src="attachment.url" controls></video>
@@ -18,7 +17,7 @@
 
     <div @click.prevent="linkClicked" v-if="type === 'html' && attachment.oembed" class="oembed">
       <div v-if="attachment.thumb_url" class="image">
-        <img :src="attachment.thumb_url"></img>
+        <img :src="attachment.thumb_url"/>
       </div>
       <div class="text">
         <h1><a :href="attachment.url">{{attachment.oembed.title}}</a></h1>
@@ -45,6 +44,10 @@
             display: flex;
           }
 
+          &.loading {
+            cursor: progress;
+          }
+
           .hider {
               position: absolute;
               margin: 10px;
@@ -111,7 +114,6 @@
               flex: 1;
 
               img {
-                  width: 100%;
                   border-style: solid;
                   border-width: 1px;
                   border-radius: 5px;

From 459fdaf10fcc55248ec2868963edcba51114c877 Mon Sep 17 00:00:00 2001
From: xj9 <xj9@heropunch.io>
Date: Wed, 8 Mar 2017 21:45:40 -0700
Subject: [PATCH 08/12] add a spin animation to favorite and boost actions

---
 src/components/favorite_button/favorite_button.js  | 14 ++++++++++++--
 src/components/favorite_button/favorite_button.vue |  4 +++-
 src/components/retweet_button/retweet_button.js    | 14 ++++++++++++--
 src/components/retweet_button/retweet_button.vue   |  1 +
 4 files changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
index 4ee3890f..466e9b84 100644
--- a/src/components/favorite_button/favorite_button.js
+++ b/src/components/favorite_button/favorite_button.js
@@ -1,5 +1,10 @@
 const FavoriteButton = {
-  props: [ 'status' ],
+  props: ['status'],
+  data () {
+    return {
+      animated: false
+    }
+  },
   methods: {
     favorite () {
       if (!this.status.favorited) {
@@ -7,13 +12,18 @@ const FavoriteButton = {
       } else {
         this.$store.dispatch('unfavorite', {id: this.status.id})
       }
+      this.animated = true
+      setTimeout(() => {
+        this.animated = false
+      }, 500)
     }
   },
   computed: {
     classes () {
       return {
         'icon-star-empty': !this.status.favorited,
-        'icon-star': this.status.favorited
+        'icon-star': this.status.favorited,
+        'animate-spin': this.animated
       }
     }
   }
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index fd53b505..0abece31 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <i :class='classes' class='favorite-button fa' v-on:click.prevent='favorite()'></i>
+    <i :class='classes' class='favorite-button fa' @click.prevent='favorite()'/>
     <span v-if='status.fave_num > 0'>{{status.fave_num}}</span>
   </div>
 </template>
@@ -10,6 +10,7 @@
 <style lang='scss'>
   .favorite-button {
       cursor: pointer;
+      animation-duration: 0.6s;
       &:hover {
         color: orange;
       }
@@ -17,4 +18,5 @@
   .icon-star {
       color: orange;
   }
+
 </style>
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
index e7318dc5..2280f315 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -1,16 +1,26 @@
 const RetweetButton = {
-  props: [ 'status' ],
+  props: ['status'],
+  data () {
+    return {
+      animated: false
+    }
+  },
   methods: {
     retweet () {
       if (!this.status.repeated) {
         this.$store.dispatch('retweet', {id: this.status.id})
       }
+      this.animated = true
+      setTimeout(() => {
+        this.animated = false
+      }, 500)
     }
   },
   computed: {
     classes () {
       return {
-        'retweeted': this.status.repeated
+        'retweeted': this.status.repeated,
+        'animate-spin': this.animated
       }
     }
   }
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 9b2f5c7b..5ec85fcf 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -11,6 +11,7 @@
   @import '../../_variables.scss';
   .icon-retweet {
      cursor: pointer;
+     animation-duration: 0.6s;
      &:hover {
       color: $green;
      }

From aff432a572c9c3368c8822a5aa5d5827eeb3b39f Mon Sep 17 00:00:00 2001
From: xj9 <xj9@heropunch.io>
Date: Wed, 8 Mar 2017 22:36:03 -0700
Subject: [PATCH 09/12] themeable hover states for nav-panel

---
 src/components/nav_panel/nav_panel.vue | 33 ++++++++++++++++++--------
 1 file changed, 23 insertions(+), 10 deletions(-)

diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 85ed163c..1d96f4d6 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -1,24 +1,24 @@
 <template>
   <div class="nav-panel">
-    <div class="panel panel-default base01-background">
+    <div class="panel panel-default base02-background">
       <ul>
         <li v-if='currentUser'>
-          <router-link to='/main/friends'>
+          <router-link class="base01-background" to='/main/friends'>
             Timeline
           </router-link>
         </li>
         <li v-if='currentUser'>
-          <router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
+          <router-link class="base01-background" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
             Mentions
           </router-link>
         </li>
         <li>
-          <router-link to='/main/public'>
+          <router-link class="base01-background" to='/main/public'>
             Public Timeline
           </router-link>
         </li>
         <li>
-          <router-link to='/main/all'>
+          <router-link class="base01-background" to='/main/all'>
             The Whole Known Network
           </router-link>
         </li>
@@ -30,7 +30,6 @@
 <script src="./nav_panel.js" ></script>
 
 <style lang="scss">
-
  .nav-panel ul {
      list-style: none;
      margin: 0;
@@ -39,7 +38,15 @@
 
  .nav-panel li {
      border-bottom: 1px solid;
-     padding: 0.8em 0.85em;
+     padding: 0;
+     &:first-child a {
+       border-top-right-radius: 10px;
+       border-top-left-radius: 10px;
+     }
+     &:last-child a {
+       border-bottom-right-radius: 10px;
+       border-bottom-left-radius: 10px;
+     }
  }
 
  .nav-panel li:last-child {
@@ -48,10 +55,16 @@
 
  .nav-panel a {
      display: block;
-     width: 100%;
-
+     padding: 0.8em 0.85em;
+     &:hover {
+       background-color: transparent;
+     }
      &.router-link-active {
-         font-weight: bold
+       font-weight: bolder;
+       background-color: transparent;
+       &:hover {
+         text-decoration: underline;
+       }
      }
  }
 

From d94cd15467d53b8f5b87381de34fd9e3d3536b45 Mon Sep 17 00:00:00 2001
From: xj9 <xj9@heropunch.io>
Date: Wed, 8 Mar 2017 22:38:14 -0700
Subject: [PATCH 10/12] fix cursor style

---
 src/components/retweet_button/retweet_button.vue | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 5ec85fcf..d923c5c4 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -17,7 +17,6 @@
      }
   }
   .retweeted {
-     cursor: auto;
      color: $green;
   }
 </style>

From 08297ea83e91418293c09e265bc87ae77d867d2a Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 9 Mar 2017 08:51:33 +0100
Subject: [PATCH 11/12] Remove redirect on login

This is to enable this workflow:

1. Open conversation in new tab
2. Login
3. Interact with the conversation

We can add this again once we have persistent logins.
---
 src/components/login_form/login_form.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index bc801397..1a6f6015 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -9,7 +9,7 @@ const LoginForm = {
   methods: {
     submit () {
       this.$store.dispatch('loginUser', this.user).then(
-        () => { this.$router.push('/main/friends')}, 
+        () => {},
         (error) => {
           this.authError = error
           this.user.username = ''

From d954909134325ef9bf0593a05117aa2787932e59 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 9 Mar 2017 08:58:17 +0100
Subject: [PATCH 12/12] Add linter.

---
 .gitlab-ci.yml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c31d2d31..296d6839 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -28,10 +28,17 @@ before_script:
 #  - node_modules/
 
 stages:
+  - lint
   - build
   - test
   - deploy
 
+lint:
+  stage: lint
+  script:
+    - yarn
+    - npm run lint
+
 test:
   stage: test
   script: