diff --git a/core/migrations/0010_auto_20180805_1139.py b/core/migrations/0010_auto_20180805_1139.py
new file mode 100644
index 0000000000000000000000000000000000000000..622d7e5a33788fcdd9993fc4149c0f164e553500
--- /dev/null
+++ b/core/migrations/0010_auto_20180805_1139.py
@@ -0,0 +1,22 @@
+# Generated by Django 2.1 on 2018-08-05 11:39
+
+import core.models
+import django.contrib.auth.models
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0009_auto_20180320_2335'),
+    ]
+
+    operations = [
+        migrations.AlterModelManagers(
+            name='useraccount',
+            managers=[
+                ('objects', django.contrib.auth.models.UserManager()),
+                ('tutors', core.models.TutorManager()),
+            ],
+        ),
+    ]
diff --git a/frontend/@types/v-clipboard/index.d.ts b/frontend/@types/v-clipboard/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f2dd4459b9daac2e8b5cffd0b18fd0fca3741346
--- /dev/null
+++ b/frontend/@types/v-clipboard/index.d.ts
@@ -0,0 +1 @@
+declare module 'v-clipboard';
\ No newline at end of file
diff --git a/frontend/src/api.js b/frontend/src/api.ts
similarity index 72%
rename from frontend/src/api.js
rename to frontend/src/api.ts
index d5965da6100864e0ef0960842eafa1cbb0e0c8e0..670854cab4becc9b07050862cbe91601ceb228b8 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.ts
@@ -1,6 +1,7 @@
 import axios from 'axios'
+import {Credentials} from '@/store/modules/authentication'
 
-function addFieldsToUrl ({url, fields = []}) {
+function addFieldsToUrl ({url, fields = []}: {url: string, fields?: string[]}) {
   return fields.length > 0 ? url + '?fields=pk,' + fields : url
 }
 
@@ -22,27 +23,27 @@ let ax = axios.create({
   }
 }
 
-export async function registerTutor (credentials) {
+export async function registerTutor (credentials: Credentials) {
   return ax.post('/api/tutor/register/', credentials)
 }
 
-export async function fetchJWT (credentials) {
-  const token = (await ax.post('/api/get-token/', credentials)).data.token
+export async function fetchJWT (credentials: Credentials): Promise<string> {
+  const token: string = (await ax.post('/api/get-token/', credentials)).data.token
   ax.defaults.headers['Authorization'] = `JWT ${token}`
   return token
 }
 
-export async function refreshJWT (token) {
-  const newToken = (await ax.post('/api/refresh-token/', {token})).data.token
+export async function refreshJWT (token: string): Promise<string> {
+  const newToken: string = (await ax.post('/api/refresh-token/', {token})).data.token
   ax.defaults.headers['Authorization'] = `JWT ${newToken}`
   return newToken
 }
 
-export async function fetchJWTTimeDelta () {
+export async function fetchJWTTimeDelta (): Promise<number> {
   return (await ax.get('/api/jwt-time-delta/')).data.timeDelta
 }
 
-export async function fetchUserRole () {
+export async function fetchUserRole (): Promise<string> {
   return (await ax.get('/api/user-role/')).data.role
 }
 
@@ -54,11 +55,11 @@ export async function fetchStudentSubmissions () {
   return (await ax.get('/api/student-submissions/')).data
 }
 
-export async function fetchSubmissionFeedbackTests ({pk}) {
+export async function fetchSubmissionFeedbackTests ({pk}: {pk: string}) {
   return (await ax.get(`/api/submission/${pk}/`)).data
 }
 
-export async function fetchAllStudents (fields = []) {
+export async function fetchAllStudents (fields: string[] = []) {
   const url = addFieldsToUrl({
     url: '/api/student/',
     fields
@@ -66,7 +67,8 @@ export async function fetchAllStudents (fields = []) {
   return (await ax.get(url)).data
 }
 
-export async function fetchStudent ({pk, fields = []}) {
+export async function fetchStudent ({pk, fields = []}:
+{pk: string, fields?: string[]}) {
   const url = addFieldsToUrl({
     url: `/api/student/${pk}/`,
     fields
@@ -74,7 +76,7 @@ export async function fetchStudent ({pk, fields = []}) {
   return (await ax.get(url)).data
 }
 
-export async function fetchAllTutors (fields = []) {
+export async function fetchAllTutors (fields: string[] = []) {
   const url = addFieldsToUrl({
     url: '/api/tutor/',
     fields
@@ -86,16 +88,16 @@ export async function fetchSubscriptions () {
   return (await ax.get('/api/subscription/')).data
 }
 
-export async function deactivateSubscription ({pk}) {
+export async function deactivateSubscription ({pk}: {pk: string}) {
   const url = `/api/subscription/${pk}/`
   return (await ax.delete(url)).data
 }
 
-export async function fetchSubscription (subscriptionPk) {
+export async function fetchSubscription (subscriptionPk: string) {
   return (await ax.get(`/api/subscription/${subscriptionPk}/`)).data
 }
 
-export async function fetchAllFeedback (fields = []) {
+export async function fetchAllFeedback (fields: string[] = []) {
   const url = addFieldsToUrl({
     url: '/api/feedback/',
     fields
@@ -108,7 +110,8 @@ export async function fetchFeedback ({ofSubmission}) {
   return (await ax.get(url)).data
 }
 
-export async function fetchExamType ({examPk, fields = []}) {
+export async function fetchExamType ({examPk, fields = []}:
+{examPk?: string, fields?: string[]}) {
   const url = addFieldsToUrl({
     url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`,
     fields})
@@ -123,8 +126,16 @@ export async function fetchStatistics (opt = {fields: []}) {
   return (await ax.get(url)).data
 }
 
-export async function subscribeTo (type, key, stage) {
-  let data = {
+interface SubscriptionPayload {
+  /* eslint-disable */
+  query_type: string,
+  query_key?: string,
+  feedback_stage?: string
+  /* eslint-enable */
+}
+
+export async function subscribeTo (type: string, key: string, stage: string) {
+  let data: SubscriptionPayload = {
     query_type: type
   }
 
@@ -189,7 +200,7 @@ export async function deactivateAllStudentAccess () {
   return ax.post('/api/student/deactivate/')
 }
 
-export async function changePassword (userPk, data) {
+export async function changePassword (userPk: string, data) {
   return ax.patch(`/api/user/${userPk}/change_password/`, data)
 }
 
@@ -197,7 +208,7 @@ export async function getOwnUser () {
   return (await ax.get('/api/user/me/')).data
 }
 
-export async function changeActiveForUser (userPk, active) {
+export async function changeActiveForUser (userPk: string, active: boolean) {
   return (await ax.patch(`/api/user/${userPk}/change_active/`, {'is_active': active})).data
 }
 
diff --git a/frontend/src/components/AutoLogout.vue b/frontend/src/components/AutoLogout.vue
index 76e695523d527f4eab20a1ba765072f1e164051e..3c29cf12caafb3c05f6213771a7f1543fccf08c5 100644
--- a/frontend/src/components/AutoLogout.vue
+++ b/frontend/src/components/AutoLogout.vue
@@ -1,7 +1,7 @@
 <template>
   <v-dialog
     persistent
-    width="fit-content"
+    max-width="30%"
     v-model="logoutDialog"
   >
     <v-card>
diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue
index c2974f03186e933cb08a8fa625a8a9ecf80607f3..87a1485174e18ffbc677a134398a15598d774210 100644
--- a/frontend/src/components/BaseLayout.vue
+++ b/frontend/src/components/BaseLayout.vue
@@ -86,8 +86,9 @@
 <script>
 import { mapGetters, mapState } from 'vuex'
 import {uiMut} from '@/store/modules/ui'
-import { createComputedGetterSetter } from '@/util/helpers'
+import { mapStateToComputedGetterSetter } from '@/util/helpers'
 import UserOptions from '@/components/UserOptions'
+
 export default {
   name: 'base-layout',
   components: {UserOptions},
@@ -99,22 +100,24 @@ export default {
       username: state => state.authentication.user.username,
       userRole: state => state.authentication.user.role
     }),
-    darkMode: createComputedGetterSetter({
-      path: 'ui.darkMode',
-      mutation: uiMut.SET_DARK_MODE
-    }),
-    darkModeUnlocked: createComputedGetterSetter({
-      path: 'ui.darkModeUnlocked',
-      mutation: uiMut.SET_DARK_MODE_UNLOCKED
+    ...mapStateToComputedGetterSetter({
+      pathPrefix: 'ui',
+      items: [
+        {
+          name: 'darkMode',
+          mutation: uiMut.SET_DARK_MODE
+        },
+        {
+          name: 'darkModeUnlocked',
+          mutation: uiMut.SET_DARK_MODE_UNLOCKED
+        },
+        {
+          name: 'mini',
+          path: 'sideBarCollapsed',
+          mutation: uiMut.SET_SIDEBAR_COLLAPSED
+        }
+      ]
     }),
-    mini: {
-      get: function () {
-        return this.$store.state.ui.sideBarCollapsed
-      },
-      set: function (collapsed) {
-        this.$store.commit(uiMut.SET_SIDEBAR_COLLAPSED, collapsed)
-      }
-    },
     production () {
       return process.env.NODE_ENV === 'production'
     },
diff --git a/frontend/src/components/CorrectionStatistics.vue b/frontend/src/components/CorrectionStatistics.vue
index 2bd836a52b442bd13c847943ca5c8d3d55891b35..c6d877b2f5178f103822e64f39fdfdb5fbdc8d75 100644
--- a/frontend/src/components/CorrectionStatistics.vue
+++ b/frontend/src/components/CorrectionStatistics.vue
@@ -7,7 +7,11 @@
         <ul class="inline-list mx-3">
           <li>Submissions per student: <span>{{statistics.submissions_per_student}}</span></li>
           <li>Submissions per type: <span>{{statistics.submissions_per_type}}</span></li>
-          <li>Curr. mean score: <span>{{statistics.current_mean_score.toFixed(2)}}</span></li>
+          <li>Curr. mean score:
+            <span>
+              {{statistics.current_mean_score === null ? 'N.A.' : statistics.current_mean_score.toFixed(2)}}
+            </span>
+          </li>
         </ul>
         <v-divider class="mx-2 my-2"></v-divider>
         <div v-for="(progress, index) in statistics.submission_type_progress" :key="index">
diff --git a/frontend/src/components/DataExport.vue b/frontend/src/components/DataExport.vue
index 2b3c58681fc4570599fab5b4d2360c922f931758..451fd950666d535aa5e4bb83a7a07bd4e88735fa 100644
--- a/frontend/src/components/DataExport.vue
+++ b/frontend/src/components/DataExport.vue
@@ -163,8 +163,4 @@ export default {
 </script>
 
 <style scoped>
-  #export-link {
-    color: #000;
-    text-decoration: none;
-  }
 </style>
diff --git a/frontend/src/components/WelcomeJumbotron.vue b/frontend/src/components/WelcomeJumbotron.vue
index d3d9151f280e4672958841fbc519fb2a9f6cea97..687274a6f14f988c3199c0982f10b60da4e9cb66 100644
--- a/frontend/src/components/WelcomeJumbotron.vue
+++ b/frontend/src/components/WelcomeJumbotron.vue
@@ -1,5 +1,10 @@
 <template>
-    <v-jumbotron :gradient="gradient" dark class="elevation-10">
+    <v-jumbotron :gradient="gradient" dark class="elevation-10" v-if="showJumbotron">
+      <v-btn @click="hide" icon class="hide-btn" absolute>
+        <v-icon>
+          close
+        </v-icon>
+      </v-btn>
       <v-container fill-height>
         <v-layout align-center>
           <v-flex>
@@ -26,12 +31,26 @@
 </template>
 
 <script>
+import { createComputedGetterSetter } from '@/util/helpers'
+import { uiMut } from '@/store/modules/ui'
+
 export default {
   name: 'welcome-jumbotron',
   data () {
     return {
       gradient: 'to bottom, #1A237E, #5753DD'
     }
+  },
+  computed: {
+    showJumbotron: createComputedGetterSetter({
+      path: 'ui.showJumbotron',
+      mutation: uiMut.SET_SHOW_JUMBOTRON
+    })
+  },
+  methods: {
+    hide () {
+      this.showJumbotron = false
+    }
   }
 }
 </script>
@@ -41,4 +60,7 @@ export default {
     color: lightgrey;
     text-decoration: none;
   }
+  .hide-btn {
+    right: 0;
+  }
 </style>
diff --git a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
index 03e7d84f650080e083e1c9bfd2a11dba814c10dc..c5ec661f53854323718c3f67843e79355b9c4756 100644
--- a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
+++ b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
@@ -88,32 +88,26 @@ export default {
       items: [
         {
           name: 'showFinal',
-          path: 'showFinal',
           mutation: feedbackSearchOptsMut.SET_SHOW_FINAL
         },
         {
           name: 'searchOtherUserComments',
-          path: 'searchOtherUserComments',
           mutation: feedbackSearchOptsMut.SET_SEARCH_OTHER_USER_COMMENTS
         },
         {
           name: 'caseSensitive',
-          path: 'caseSensitive',
           mutation: feedbackSearchOptsMut.SET_CASE_SENSITIVE
         },
         {
           name: 'useRegex',
-          path: 'useRegex',
           mutation: feedbackSearchOptsMut.SET_USE_REGEX
         },
         {
           name: 'filterByTutors',
-          path: 'filterByTutors',
           mutation: feedbackSearchOptsMut.SET_FILTER_BY_TUTORS
         },
         {
           name: 'filterByStage',
-          path: 'filterByStage',
           mutation: feedbackSearchOptsMut.SET_FILTER_BY_STAGE
         }
 
diff --git a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
index ce5a4b26ded02686037e41fa744996fae411e8a8..362980ee6d7a14ad660374a0f8f25a67d6424e48 100644
--- a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
+++ b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
@@ -41,10 +41,8 @@ export default {
   watch: {
     nextRoute (newVal, oldVal) {
       if (newVal !== oldVal && this.$store.getters['submissionNotes/workInProgress']) {
-        console.log('here')
         this.dialog = true
       } else {
-        console.log('there')
         this.nextRoute()
       }
     }
diff --git a/frontend/src/components/submission_notes/base/CommentForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue
index a8c1949df1446b1eb9f0757a7f52ade8c6437439..603f4398ff7b973165c873b1812c844dbc50a699 100644
--- a/frontend/src/components/submission_notes/base/CommentForm.vue
+++ b/frontend/src/components/submission_notes/base/CommentForm.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <v-text-field
+    <v-textarea
       name="feedback-input"
       label="Please provide your feedback here"
       v-model="currentFeedback"
@@ -8,7 +8,7 @@
       @keyup.esc="collapseTextField"
       @focus="selectInput($event)"
       rows="2"
-      textarea
+      outline
       autofocus
       auto-grow
       hide-details
diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue
index 935914856e729f745b3109104d6d0724d7033502..5b29b56c4b888872efd4ec093d36f79113d94525 100644
--- a/frontend/src/components/submission_notes/base/FeedbackComment.vue
+++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue
@@ -21,7 +21,7 @@
       </div>
       <div class="message">{{text}}</div>
       <v-btn
-        flat icon
+        flat icon absolute
         class="delete-button"
         v-if="deletable"
         @click.stop="toggleDeleteComment"
@@ -144,9 +144,8 @@ export default {
     white-space: pre-wrap;
   }
   .delete-button {
-    position: absolute;
-    bottom: -20px;
-    left: -50px;
+    bottom: -12px;
+    left: -42px;
   }
   .comment-created {
     position: absolute;
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
index 4ce05b34206cd19bf26c11647770840247088887..2e8b7f72b1a704b90fea1c8bd582e3bd2aa0b371 100644
--- a/frontend/src/components/subscriptions/SubscriptionList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -2,7 +2,7 @@
   <v-card>
     <v-toolbar color="teal" :dense="sidebar">
       <v-toolbar-side-icon><v-icon>assignment</v-icon></v-toolbar-side-icon>
-      <v-toolbar-title v-if="!sidebar || (sidebar && !sideBarCollapsed)" style="min-width: fit-content;">
+      <v-toolbar-title v-if="showDetail" style="min-width: fit-content;">
         Tasks
       </v-toolbar-title>
       <v-spacer/>
@@ -16,7 +16,7 @@
         />
       </v-btn>
     </v-toolbar>
-    <v-tabs grow color="teal lighten-1" v-model="selectedStage">
+    <v-tabs grow color="teal lighten-1" v-model="selectedStage" v-if="showDetail">
       <v-tab v-for="(item, i) in stagesReadable" :key="i">
         {{item}}
       </v-tab>
@@ -59,7 +59,10 @@ export default {
       subscriptions: 'getSubscriptionsGroupedByType',
       stages: 'availableStages',
       stagesReadable: 'availableStagesReadable'
-    })
+    }),
+    showDetail () {
+      return !this.sidebar || (this.sidebar && !this.sideBarCollapsed)
+    }
   },
   methods: {
     ...mapActions([
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 5576736f60685a98f689a1ee5caea62c9a2c278a..ad81436ec767936f2e0d2118a82e08287f7d99f8 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -4,13 +4,13 @@ import router from './router/index'
 import store from './store/store'
 import Vuetify from 'vuetify'
 import Notifications from 'vue-notification'
-import Cliboard from 'v-clipboard'
+import Clipboard from 'v-clipboard'
 
 import 'vuetify/dist/vuetify.min.css'
 import 'highlight.js/styles/atom-one-light.css'
 
 Vue.use(Vuetify)
-Vue.use(Cliboard)
+Vue.use(Clipboard)
 Vue.use(Notifications)
 
 Vue.config.productionTip = false
diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue
index 972b08b5086bbb1d9ffb3e37f7ac240751bebd23..0988645e495fe611959f06788d662156c367236b 100644
--- a/frontend/src/pages/Login.vue
+++ b/frontend/src/pages/Login.vue
@@ -1,7 +1,7 @@
 <template>
   <v-container fill-height>
     <v-layout align-center justify-center>
-      <v-dialog v-model="registerDialog" max-width="fit-content" class="pa-4">
+      <v-dialog v-model="registerDialog" class="pa-4" max-width="30%">
         <register-dialog @registered="registered($event)"/>
       </v-dialog>
       <v-flex text-xs-center xs8 sm6 md4 lg2>
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index e780522f3cb6e3a2723bb573f3b59c3304a3e7ec..25b1aeee5b8f2fa7288587de7e8c2e923231c0d9 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -1,5 +1,5 @@
 import Vue from 'vue'
-import Router from 'vue-router'
+import Router, {RawLocation, Route, NavigationGuard} from 'vue-router'
 import Login from '@/pages/Login.vue'
 import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage.vue'
 import StudentOverviewPage from '@/pages/reviewer/StudentOverviewPage.vue'
@@ -19,7 +19,9 @@ import store from '@/store/store'
 
 Vue.use(Router)
 
-function denyAccess (next, redirect) {
+type rerouteFunc = (to?: RawLocation | false | ((vm: Vue) => any) | void) => void
+
+function denyAccess (next: rerouteFunc, redirect: Route) {
   next(redirect.path)
   VueInstance.$notify({
     title: 'Access denied',
@@ -28,23 +30,23 @@ function denyAccess (next, redirect) {
   })
 }
 
-function tutorOrReviewerOnly (to, from, next) {
+let tutorOrReviewerOnly: NavigationGuard = function (to, from, next) {
   if (store.getters.isTutorOrReviewer) {
     next()
   } else {
-    denyAccess(next, from.path)
+    denyAccess(next, from)
   }
 }
 
-function reviewerOnly (to, from, next) {
+let reviewerOnly: NavigationGuard = function (to, from, next) {
   if (store.getters.isReviewer) {
     next()
   } else {
-    denyAccess(next, from.path)
+    denyAccess(next, from)
   }
 }
 
-function studentOnly (to, from, next) {
+let studentOnly: NavigationGuard = function (to, from, next) {
   if (store.getters.isStudent) {
     next()
   } else {
@@ -52,7 +54,7 @@ function studentOnly (to, from, next) {
   }
 }
 
-function checkLoggedIn (to, from, next) {
+let checkLoggedIn: NavigationGuard = function (to, from, next) {
   if (store.getters.isLoggedIn) {
     next()
   } else {
diff --git a/frontend/src/store/actions.js b/frontend/src/store/actions.ts
similarity index 96%
rename from frontend/src/store/actions.js
rename to frontend/src/store/actions.ts
index b6ce78791497a13300e2b9eb1f9728530193d0e0..47ca46fc750f2160e3a009bd6ebc70784d4d6323 100644
--- a/frontend/src/store/actions.js
+++ b/frontend/src/store/actions.ts
@@ -53,9 +53,9 @@ const actions = {
       handleError(err, dispatch, 'Unable to fetch tutor data')
     }
   },
-  async getAllFeedback ({commit, dispatch}, fields = []) {
+  async getAllFeedback ({commit, dispatch}, fields: string[] = []) {
     try {
-      const feedback = await api.fetchAllFeedback({fields})
+      const feedback = await api.fetchAllFeedback(fields)
       commit(mut.SET_ALL_FEEDBACK, feedback)
       commit(mut.MAP_FEEDBACK_OF_SUBMISSION_TYPE)
     } catch (err) {
@@ -110,7 +110,7 @@ const actions = {
     commit('submissionNotes/' + subNotesMut.RESET_STATE)
     commit(authMut.SET_MESSAGE, message)
     router.push({name: 'login'})
-    router.go()
+    router.go(0)
   }
 }
 
diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.ts
similarity index 84%
rename from frontend/src/store/getters.js
rename to frontend/src/store/getters.ts
index 5663746c1d79ac91d22c6b369f3455a9e46ed99b..3cdc0cbe2850114a1e33d632a759cd4a2da74a4c 100644
--- a/frontend/src/store/getters.js
+++ b/frontend/src/store/getters.ts
@@ -1,7 +1,7 @@
 const getters = {
   corrected (state) {
     return state.statistics.submission_type_progress.every(progress => {
-      return progress.percentage === 100
+      return progress.feedback_final === progress.submission_count
     })
   },
   getSubmission: state => pk => {
diff --git a/frontend/src/store/grady_speak.js b/frontend/src/store/grady_speak.ts
similarity index 100%
rename from frontend/src/store/grady_speak.js
rename to frontend/src/store/grady_speak.ts
diff --git a/frontend/src/store/modules/authentication.js b/frontend/src/store/modules/authentication.ts
similarity index 68%
rename from frontend/src/store/modules/authentication.js
rename to frontend/src/store/modules/authentication.ts
index ce8bbb045e98a85a10ec70e979ce41f116b25b11..9e0c95e7234e7b27895120c20f2982382f2e8d50 100644
--- a/frontend/src/store/modules/authentication.js
+++ b/frontend/src/store/modules/authentication.ts
@@ -1,9 +1,28 @@
 import * as api from '@/api'
 import gradySays from '../grady_speak'
+import {ActionContext, Module} from 'vuex'
 
-function initialState () {
+export interface Credentials {
+    username: string,
+    password: string
+}
+
+interface AuthState {
+    token: string,
+    lastTokenRefreshTry: number,
+    refreshingToken: boolean,
+    jwtTimeDelta: number,
+    message: string,
+    user: {
+        pk: string,
+        username: string,
+        role: string,
+        is_admin: boolean //eslint-disable-line
+    }
+}
+function initialState (): AuthState {
   return {
-    token: sessionStorage.getItem('token'),
+    token: sessionStorage.getItem('token') || '',
     lastTokenRefreshTry: Date.now(),
     refreshingToken: false,
     jwtTimeDelta: 0,
@@ -12,7 +31,7 @@ function initialState () {
       pk: '',
       username: '',
       role: '',
-      is_admin: ''
+      is_admin: false
     }
   }
 }
@@ -27,53 +46,53 @@ export const authMut = Object.freeze({
   SET_REFRESHING_TOKEN: 'SET_REFRESHING_TOKEN'
 })
 
-const authentication = {
+const authentication: Module<AuthState, any> = {
   state: initialState(),
   getters: {
     gradySpeak: () => {
       return gradySays[Math.floor(Math.random() * gradySays.length)]
     },
-    isStudent: state => {
+    isStudent: (state: AuthState) => {
       return state.user.role === 'Student'
     },
-    isTutor: state => {
+    isTutor: (state: AuthState) => {
       return state.user.role === 'Tutor'
     },
-    isReviewer: state => {
+    isReviewer: (state: AuthState) => {
       return state.user.role === 'Reviewer'
     },
-    isTutorOrReviewer: (state, getters) => {
+    isTutorOrReviewer: (state: AuthState, getters) => {
       return getters.isTutor || getters.isReviewer
     },
-    isLoggedIn: state => !!state.token
+    isLoggedIn: (state: AuthState) => !!state.token
   },
   mutations: {
-    [authMut.SET_MESSAGE] (state, message) {
+    [authMut.SET_MESSAGE] (state: AuthState, message: string) {
       state.message = message
     },
-    [authMut.SET_JWT_TOKEN] (state, token) {
+    [authMut.SET_JWT_TOKEN] (state: AuthState, token: string) {
       sessionStorage.setItem('token', token)
       state.token = token
     },
-    [authMut.SET_JWT_TIME_DELTA] (state, timeDelta) {
+    [authMut.SET_JWT_TIME_DELTA] (state: AuthState, timeDelta: number) {
       state.jwtTimeDelta = timeDelta
     },
-    [authMut.SET_USER] (state, user) {
+    [authMut.SET_USER] (state: AuthState, user) {
       state.user = user
     },
-    [authMut.SET_REFRESHING_TOKEN] (state, refreshing) {
+    [authMut.SET_REFRESHING_TOKEN] (state: AuthState, refreshing: boolean) {
       state.refreshingToken = refreshing
     },
-    [authMut.SET_LAST_TOKEN_REFRESH_TRY] (state) {
+    [authMut.SET_LAST_TOKEN_REFRESH_TRY] (state: AuthState) {
       state.lastTokenRefreshTry = Date.now()
     },
-    [authMut.RESET_STATE] (state) {
+    [authMut.RESET_STATE] (state: AuthState) {
       sessionStorage.setItem('token', '')
       Object.assign(state, initialState())
     }
   },
   actions: {
-    async getJWT (context, credentials) {
+    async getJWT (context: ActionContext<AuthState, any>, credentials: Credentials) {
       try {
         const token = await api.fetchJWT(credentials)
         context.commit(authMut.SET_JWT_TOKEN, token)
diff --git a/frontend/src/store/modules/feedback_list/feedback-search-options.js b/frontend/src/store/modules/feedback_list/feedback-search-options.ts
similarity index 100%
rename from frontend/src/store/modules/feedback_list/feedback-search-options.js
rename to frontend/src/store/modules/feedback_list/feedback-search-options.ts
diff --git a/frontend/src/store/modules/feedback_list/feedback-table.js b/frontend/src/store/modules/feedback_list/feedback-table.ts
similarity index 96%
rename from frontend/src/store/modules/feedback_list/feedback-table.js
rename to frontend/src/store/modules/feedback_list/feedback-table.ts
index 35e526ac8befd77a62e77e5ab6b78d74cb3c7dca..4ea766a8009ccf177b7c464fa246545e2ade6e85 100644
--- a/frontend/src/store/modules/feedback_list/feedback-table.js
+++ b/frontend/src/store/modules/feedback_list/feedback-table.ts
@@ -41,7 +41,7 @@ const feedbackTable = {
   actions: {
     mapFeedbackHistOfSubmissionType ({getters, commit, state}) {
       for (const feedback of Object.values(state.feedbackHist)) {
-        const type = getters.getSubmissionType(feedback.of_submission_type)
+        const type = getters.getSubmissionType((feedback as any).of_submission_type)
         commit(feedbackTableMut.SET_FEEDBACK_OF_SUBMISSION_TYPE, {feedback, type})
       }
     },
diff --git a/frontend/src/store/modules/student-page.js b/frontend/src/store/modules/student-page.ts
similarity index 100%
rename from frontend/src/store/modules/student-page.js
rename to frontend/src/store/modules/student-page.ts
diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.ts
similarity index 98%
rename from frontend/src/store/modules/submission-notes.js
rename to frontend/src/store/modules/submission-notes.ts
index 08669ce051587f7b08620ac603d668d863dfddf3..cd02557f0009049f8381fe55c6e660e4cd2a8bfd 100644
--- a/frontend/src/store/modules/submission-notes.js
+++ b/frontend/src/store/modules/submission-notes.ts
@@ -122,7 +122,7 @@ const submissionNotes = {
     deleteComments: async function ({state}) {
       return Promise.all(
         Object.values(state.commentsMarkedForDeletion).map(comment => {
-          return api.deleteComment(comment)
+          return api.deleteComment((comment as any))
         })
       )
     },
@@ -143,7 +143,7 @@ const submissionNotes = {
       if (!state.hasOrigFeedback) {
         return api.submitFeedbackForAssignment({feedback})
       } else {
-        feedback.pk = state.origFeedback.pk
+        feedback['pk']= state.origFeedback.pk
         return api.submitUpdatedFeedback({feedback})
       }
     }
diff --git a/frontend/src/store/modules/subscriptions.js b/frontend/src/store/modules/subscriptions.ts
similarity index 91%
rename from frontend/src/store/modules/subscriptions.js
rename to frontend/src/store/modules/subscriptions.ts
index 531ea8093b91a03d64fb41f56960d1eea10a007e..588867f0defa7321fd29e461fa3e2d611db0cad6 100644
--- a/frontend/src/store/modules/subscriptions.js
+++ b/frontend/src/store/modules/subscriptions.ts
@@ -50,10 +50,10 @@ const subscriptions = {
       return stages
     },
     availableSubmissionTypeQueryKeys (state, getters, rootState) {
-      return Object.values(rootState.submissionTypes).map(subType => subType.pk)
+      return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk)
     },
     availableExamTypeQueryKeys (state, getters, rootState) {
-      return Object.values(rootState.examTypes).map(examType => examType.pk)
+      return Object.values(rootState.examTypes).map((examType: any) => examType.pk)
     },
     activeSubscription (state) {
       return state.subscriptions[state.activeSubscriptionPk]
@@ -86,25 +86,26 @@ const subscriptions = {
         acc[curr] = subscriptionsByType()
         return acc
       }, {})
-      Object.values(state.subscriptions).forEach(subscription => {
+      Object.values(state.subscriptions).forEach((subscription: any) => {
         subscriptionsByStage[subscription.feedback_stage][subscription.query_type].push(subscription)
       })
       // sort the resulting arrays in subscriptions lexicographically by their query_keys
-      const sortSubscriptions = subscriptionsByType => Object.values(subscriptionsByType).forEach(arr => {
-        if (arr.length > 1 && arr[0].hasOwnProperty('query_key')) {
-          arr.sort((subA, subB) => {
-            const subALower = getters.resolveSubscriptionKeyToName(subA).toLowerCase()
-            const subBLower = getters.resolveSubscriptionKeyToName(subB).toLowerCase()
-            if (subALower < subBLower) {
-              return -1
-            } else if (subALower > subBLower) {
-              return 1
-            } else {
-              return 0
-            }
-          })
-        }
-      })
+      const sortSubscriptions = subscriptionsByType => Object.values(subscriptionsByType)
+        .forEach((arr: object[]) => {
+          if (arr.length > 1 && arr[0].hasOwnProperty('query_key')) {
+            arr.sort((subA, subB) => {
+              const subALower = getters.resolveSubscriptionKeyToName(subA).toLowerCase()
+              const subBLower = getters.resolveSubscriptionKeyToName(subB).toLowerCase()
+              if (subALower < subBLower) {
+                return -1
+              } else if (subALower > subBLower) {
+                return 1
+              } else {
+                return 0
+              }
+            })
+          }
+        })
       Object.values(subscriptionsByStage).forEach(subscriptionsByType => {
         sortSubscriptions(subscriptionsByType)
       })
@@ -186,7 +187,7 @@ const subscriptions = {
       }
     },
     async cleanAssignmentsFromSubscriptions ({commit, state, dispatch}, excludeActive = true) {
-      Object.values(state.subscriptions).forEach(subscription => {
+      Object.values(state.subscriptions).forEach((subscription: any) => {
         if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) {
           subscription.assignments.forEach(assignment => {
             api.deleteAssignment({assignment})
diff --git a/frontend/src/store/modules/ui.js b/frontend/src/store/modules/ui.ts
similarity index 70%
rename from frontend/src/store/modules/ui.js
rename to frontend/src/store/modules/ui.ts
index df08d683c22f997f988e7ba764f57654f9b54e41..2cecc601d8df26034b9d98da0fc3547a44d4ce84 100644
--- a/frontend/src/store/modules/ui.js
+++ b/frontend/src/store/modules/ui.ts
@@ -3,14 +3,16 @@ function initialState () {
   return {
     sideBarCollapsed: false,
     darkMode: false,
-    darkModeUnlocked: false
+    darkModeUnlocked: false,
+    showJumbotron: true
   }
 }
 
 export const uiMut = Object.freeze({
   SET_SIDEBAR_COLLAPSED: 'SET_SIDEBAR_COLLAPSED',
   SET_DARK_MODE: 'SET_DARK_MODE',
-  SET_DARK_MODE_UNLOCKED: 'SET_DARK_MODE_UNLOCKED'
+  SET_DARK_MODE_UNLOCKED: 'SET_DARK_MODE_UNLOCKED',
+  SET_SHOW_JUMBOTRON: 'SET_SHOW_JUMBOTRON'
 })
 
 const ui = {
@@ -24,6 +26,9 @@ const ui = {
     },
     [uiMut.SET_DARK_MODE_UNLOCKED] (state, val) {
       state.darkModeUnlocked = val
+    },
+    [uiMut.SET_SHOW_JUMBOTRON] (state, val) {
+      state.showJumbotron = val
     }
   }
 }
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.ts
similarity index 79%
rename from frontend/src/store/mutations.js
rename to frontend/src/store/mutations.ts
index 2e4c4081fd4253ef781e21b22e62589baefdc798..1c9e80ad6460bd0813b5e60872df2403f5f74508 100644
--- a/frontend/src/store/mutations.js
+++ b/frontend/src/store/mutations.ts
@@ -26,27 +26,6 @@ const mutations = {
       return acc
     }, {})
   },
-  [mut.SET_SUBSCRIPTIONS] (state, subscriptions) {
-    state.subscriptions = subscriptions.reduce((acc, curr) => {
-      acc[curr['pk']] = curr
-      return acc
-    }, {})
-    for (let subscription of Object.values(subscriptions)) {
-      for (let assignment of subscription.assignments) {
-        if (assignment.feedback) {
-          Vue.set(assignment.feedback, 'feedback_stage_for_user', subscription.feedback_stage)
-        }
-      }
-    }
-  },
-  [mut.SET_SUBSCRIPTION] (state, subscription) {
-    Vue.set(state.subscriptions, subscription.pk, subscription)
-    for (let assignment of subscription.assignments) {
-      if (assignment.feedback) {
-        Vue.set(assignment.feedback, 'feedback_stage_for_user', subscription.feedback_stage)
-      }
-    }
-  },
   [mut.SET_STUDENTS] (state, students) {
     state.students = students.reduce((acc, curr) => {
       acc[curr.pk] = mapStudent(curr, state.studentMap)
diff --git a/frontend/src/store/plugins/lastInteractionPlugin.js b/frontend/src/store/plugins/lastInteractionPlugin.ts
similarity index 100%
rename from frontend/src/store/plugins/lastInteractionPlugin.js
rename to frontend/src/store/plugins/lastInteractionPlugin.ts
diff --git a/frontend/src/util/helpers.js b/frontend/src/util/helpers.ts
similarity index 60%
rename from frontend/src/util/helpers.js
rename to frontend/src/util/helpers.ts
index c18c724fc35c10616f5a168b7628d7adc1a1fc83..1927f50accbc2661ba4cd552c8be1db8711540ea 100644
--- a/frontend/src/util/helpers.js
+++ b/frontend/src/util/helpers.ts
@@ -1,11 +1,12 @@
+import vueInstance from '@/main.ts'
 
-export function nameSpacer (namespace) {
-  return function (commitType) {
+export function nameSpacer (namespace: string) {
+  return function (commitType: string) {
     return namespace + commitType
   }
 }
 
-export function getObjectValueByPath (obj, path) {
+export function getObjectValueByPath (obj: any, path: string): any {
   // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621
   if (!path || path.constructor !== String) return
   path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
@@ -22,6 +23,11 @@ export function getObjectValueByPath (obj, path) {
   return obj
 }
 
+interface GetSetPair {
+    get: () => any,
+    set: (val: object) => void
+}
+
 /**
  * Use this method to generate a computed property accessing the store for a Vue instance.
  * The get method will return the value at this.$store.state.<path>.
@@ -31,17 +37,29 @@ export function getObjectValueByPath (obj, path) {
  * @param namespace to prepend the mutation type with
  * @returns {*}
  */
-export function createComputedGetterSetter ({path, mutation, namespace}) {
+export function createComputedGetterSetter (
+  {path, mutation, namespace}:
+  {path: string, mutation: string, namespace:string}): GetSetPair {
   return {
-    get () {
-      return getObjectValueByPath(this.$store.state, path)
+    get (): any {
+      return getObjectValueByPath(vueInstance.$store.state, path)
     },
-    set (val) {
-      this.$store.commit(`${namespace ? namespace + '/' : ''}${mutation}`, val)
+    set (val: object): void {
+      vueInstance.$store.commit(`${namespace ? namespace + '/' : ''}${mutation}`, val)
     }
   }
 }
 
+interface StateMapperItem {
+    name: string,
+    mutation: string,
+    path?: string
+}
+
+interface MappedState {
+    [key: string]: GetSetPair
+}
+
 /**
  * Returns an object of generated computed getter/setter pairs.
  * Can be used to quickly bind a stores state and corresponding setters to a vue component
@@ -49,10 +67,14 @@ export function createComputedGetterSetter ({path, mutation, namespace}) {
  * @param pathPrefix if set, all items path will be prepended by the path prefix
  * @param items array that contains objects {name, path, mutation}
  */
-export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = '', items = []}) {
-  return items.reduce((acc, curr) => {
-    let path = pathPrefix ? `${pathPrefix}.${curr.path}` : curr.path
-    acc[curr.name] = createComputedGetterSetter({...curr, path, namespace})
+export function mapStateToComputedGetterSetter (
+  {namespace = '', pathPrefix = '', items = []}:
+  {namespace: string, pathPrefix: string, items: StateMapperItem[]}): MappedState {
+  return items.reduce((acc: MappedState, curr) => {
+    // if no path is give, use name
+    const itemPath = curr.path || curr.name
+    const path = pathPrefix ? `${pathPrefix}.${itemPath}` : itemPath
+    acc[curr.name] = createComputedGetterSetter({mutation: curr.mutation, path, namespace})
     return acc
   }, {})
 }
@@ -61,24 +83,25 @@ export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = ''
 // https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript/43053803#43053803
 let cartesianHelper = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b))))
 export function cartesian (a, b, ...c) {
+  // @ts-ignore can be ignored since cartesian is only recursively called if b si truthy
   return b ? cartesian(cartesianHelper(a, b), ...c) : a
 }
 
 // flatten an array
-export function flatten (list) {
+export function flatten (list: any[]): any[] {
   return list.reduce(
     (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
   )
 }
 
-export function objectifyArray (arr, key = 'pk') {
+export function objectifyArray<T> (arr: T[], key = 'pk'): {[key: string]: T} {
   return arr.reduce((acc, curr) => {
     acc[curr[key]] = curr
     return acc
   }, {})
 }
 
-export function once (fn, context) {
+export function once (fn: Function, context?: object) {
   let result
   return function () {
     if (!result) {
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index c1700466f6d20ad84535501ff813e85d1d5cc680..c800f3ea8e347d2dbec47be5b999bd78f2e119d3 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -10,24 +10,20 @@
     "esModuleInterop": true,
     "sourceMap": true,
     "baseUrl": ".",
-    "types": [
-      "node",
-      "mocha",
-      "chai"
-    ],
     "paths": {
       "@/*": [
         "src/*"
       ]
     },
     "lib": [
-      "es2015",
+      "es2017",
       "dom",
       "dom.iterable",
       "scripthost"
     ]
   },
   "include": [
+    "@types/",
     "src/**/*.ts",
     "src/**/*.tsx",
     "src/**/*.vue",
diff --git a/frontend/vue.config.js b/frontend/vue.config.js
index 78bc4df2ae1e5ca245453ca7b8c389cee5e513c8..d321253997a1fc75cc8b2ed0b9aabd624ded3303 100644
--- a/frontend/vue.config.js
+++ b/frontend/vue.config.js
@@ -3,6 +3,11 @@ const path = require('path')
 const projectRoot = path.resolve(__dirname)
 
 module.exports = {
+  assetsDir: 'static',
+  devServer: {
+    allowedHosts: ['localhost'],
+    host: 'localhost'
+  },
   configureWebpack: {
     resolve: {
       alias: {
diff --git a/grady/settings/default.py b/grady/settings/default.py
index 8099ca8962d5f00b7cefae7ca566d27a2b51332d..eb220958dc8b6186385e41cc9dc0fc3a49a010ae 100644
--- a/grady/settings/default.py
+++ b/grady/settings/default.py
@@ -116,7 +116,7 @@ STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
 )
 STATICFILES_DIRS = (
-    'frontend/dist/',
+    'frontend/dist/static',
 )