diff --git a/core/tests/test_access_rights.py b/core/tests/test_access_rights.py
index 6e172626ff78f4374547336ebd2399e9c0461b09..9c22248b907e106d7a78448fb011655a04b5bb45 100644
--- a/core/tests/test_access_rights.py
+++ b/core/tests/test_access_rights.py
@@ -4,7 +4,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase,
                                  force_authenticate)
 
 from core.views import (ExamApiViewSet, StudentReviewerApiViewSet,
-                        StudentSelfApiViewSet, TutorApiViewSet)
+                        StudentSelfApiView, TutorApiViewSet)
 from util.factories import GradyUserFactory
 
 
@@ -21,8 +21,8 @@ class AccessRightsOfStudentAPIViewTests(APITestCase):
         self.student = self.user_factory.make_student()
         self.tutor = self.user_factory.make_tutor()
         self.reviewer = self.user_factory.make_reviewer()
-        self.request = self.factory.get(reverse('student_page-list'))
-        self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'})
+        self.request = self.factory.get(reverse('student-page'))
+        self.view = StudentSelfApiView.as_view()
 
     def test_unauthenticated_access_denied(self):
         response = self.view(self.request)
diff --git a/core/tests/test_functional_views.py b/core/tests/test_functional_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..f48645b23b3ba0f542a86ba517c1b133b08f1eda
--- /dev/null
+++ b/core/tests/test_functional_views.py
@@ -0,0 +1,33 @@
+from django.urls import reverse
+from rest_framework.test import (APIRequestFactory, APITestCase,
+                                 force_authenticate)
+from core.views import get_user_role
+from util.factories import GradyUserFactory
+
+
+class GetUserRoleTest(APITestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.factory = APIRequestFactory()
+        cls.user_factory = GradyUserFactory()
+        cls.student = cls.user_factory.make_student()
+        cls.tutor = cls.user_factory.make_tutor()
+        cls.reviewer = cls.user_factory.make_reviewer()
+
+    def setUp(self):
+        self.request = self.factory.get(reverse('user-role'))
+
+    def test_get_user_model_returns_student(self):
+        force_authenticate(self.request, user=self.student.user)
+        response = get_user_role(self.request)
+        self.assertEqual(response.data['role'], 'Student')
+
+    def test_get_user_model_returns_tutor(self):
+        force_authenticate(self.request, user=self.tutor.user)
+        response = get_user_role(self.request)
+        self.assertEqual(response.data['role'], 'Tutor')
+
+    def test_get_user_model_returns_reviewer(self):
+        force_authenticate(self.request, user=self.reviewer.user)
+        response = get_user_role(self.request)
+        self.assertEqual(response.data['role'], 'Reviewer')
diff --git a/core/tests/test_student_page.py b/core/tests/test_student_page.py
index 930163ad8c94ebb906210976e06f6de8253c3b14..099c54cd6d54d4bb4b910d4feee6180395274433 100644
--- a/core/tests/test_student_page.py
+++ b/core/tests/test_student_page.py
@@ -4,7 +4,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase,
 
 from core.models import Reviewer, SubmissionType
 from core.tests import data_factories
-from core.views import StudentSelfApiViewSet
+from core.views import StudentSelfApiView
 
 
 class StudentPageTests(APITestCase):
@@ -19,8 +19,8 @@ class StudentPageTests(APITestCase):
         self.student = self.submission.student
         self.reviewer = Reviewer.objects.create(
             user=data_factories.make_user(username='reviewer'))
-        self.request = self.factory.get(reverse('student_page-list'))
-        self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'})
+        self.request = self.factory.get(reverse('student-page'))
+        self.view = StudentSelfApiView.as_view()
 
         force_authenticate(self.request, user=self.student.user)
         self.response = self.view(self.request)
diff --git a/core/urls.py b/core/urls.py
index 845578227821504d6eb67111ecf6ea7c33593140..d0e251b70cbc95adbd26041e484dcc9b643c1c4e 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -12,11 +12,17 @@ router.register(r'student', views.StudentReviewerApiViewSet)
 router.register(r'examtype', views.ExamApiViewSet)
 router.register(r'submissiontype', views.SubmissionTypeApiView)
 router.register(r'tutor', views.TutorApiViewSet)
-router.register(r'student-page', views.StudentSelfApiViewSet,
-                base_name='student_page')
+
+# regular views that are not viewsets
+regular_views_urlpatterns = [
+    url(r'student-page', views.StudentSelfApiView.as_view(), name='student-page'),
+    url(r'user-role', views.get_user_role, name='user-role'),
+    url(r'jwt-time-delta', views.get_jwt_expiration_delta, name='jwt-time-delta')
+]
 
 urlpatterns = [
     url(r'^api/', include(router.urls)),
+    url(r'^api/', include(regular_views_urlpatterns)),
     url(r'^api-token-auth/', obtain_jwt_token),
     url(r'^api-token-refresh', refresh_jwt_token),
     url(r'^$', TemplateView.as_view(template_name='index.html')),
diff --git a/core/views.py b/core/views.py
index 563331875203a5312d98bbc74e4e5e5afa15ee40..32bd7f1263e3e4bedb6719e171846e7b356d1da5 100644
--- a/core/views.py
+++ b/core/views.py
@@ -1,7 +1,10 @@
 """ All API views that are used to retrieve data from the database. They
 can be categorized by the permissions they require. All views require a
 user to be authenticated and most are only accessible by one user group """
-from rest_framework import mixins, viewsets
+from django.conf import settings
+from rest_framework import mixins, viewsets, generics
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
 
 from core.models import ExamType, Student, SubmissionType, Tutor
 from core.permissions import IsReviewer, IsStudent
@@ -10,10 +13,19 @@ from core.serializers import (ExamSerializer, StudentSerializer,
                               SubmissionTypeSerializer, TutorSerializer)
 
 
-class StudentSelfApiViewSet(viewsets.ReadOnlyModelViewSet):
+@api_view()
+def get_jwt_expiration_delta(request):
+    return Response({'timeDelta': settings.JWT_AUTH['JWT_EXPIRATION_DELTA']})
+
+
+@api_view()
+def get_user_role(request):
+    return Response({'role': type(request.user.get_associated_user()).__name__})
+
+
+class StudentSelfApiView(generics.RetrieveAPIView):
     """ Gets all data that belongs to one student """
     permission_classes = (IsStudent,)
-    queryset = Student.objects.all()
     serializer_class = StudentSerializer
 
     def get_object(self) -> Student:
diff --git a/frontend/config/index.js b/frontend/config/index.js
index 2c458f66802f5f8d206f4f136a0b5d52b59b4c21..91cda89ee4dffe53254ba613294b6df8b9d1bc4b 100644
--- a/frontend/config/index.js
+++ b/frontend/config/index.js
@@ -17,7 +17,7 @@ module.exports = {
     // Surge or Netlify already gzip all static assets for you.
     // Before setting to `true`, make sure to:
     // npm install --save-dev compression-webpack-plugin
-    productionGzip: false,
+    productionGzip: true,
     productionGzipExtensions: ['js', 'css'],
     // Run the build command with an extra argument to
     // View the bundle analyzer report after build finishes:
diff --git a/frontend/index.html b/frontend/index.html
index fb63c5378340aee23aac55a9168ec7067cb63066..c2b9135d7c4243df5b2e5ae26ecb8616aeae41f8 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,7 +2,7 @@
 <html>
   <head>
     <meta charset="utf-8">
-    <title>frontend</title>
+    <title>Grady</title>
   </head>
   <body>
     <div id="app"></div>
diff --git a/frontend/package.json b/frontend/package.json
index 9a8b2be5838dca92a043717b4fb8e678d35a8756..9f4245d5f207a1c6fca82b536c426d6f8460c366 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,6 +14,8 @@
   },
   "dependencies": {
     "axios": "^0.17.0",
+    "google-code-prettify": "^1.0.5",
+    "material-design-icons": "^3.0.1",
     "vue": "^2.5.2",
     "vue-router": "^3.0.1",
     "vuetify": "^0.17.3",
@@ -31,6 +33,7 @@
     "babel-register": "^6.22.0",
     "chai": "^4.1.2",
     "chalk": "^2.0.1",
+    "compression-webpack-plugin": "^1.0.1",
     "connect-history-api-fallback": "^1.3.0",
     "copy-webpack-plugin": "^4.0.1",
     "cross-env": "^5.0.1",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 64fcd76cf6a10610db451677e80912d837ffaeae..47893229c88166fe70838840516122f9687ae87a 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -11,11 +11,8 @@
     name: 'app',
     components: {
     }
-}
+  }
 </script>
 
 <style>
-#app {
-
-}
 </style>
diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png
deleted file mode 100644
index f3d2503fc2a44b5053b0837ebea6e87a2d339a43..0000000000000000000000000000000000000000
Binary files a/frontend/src/assets/logo.png and /dev/null differ
diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue
index fddc17eecc103aa5bac87342fbd1e78fb165eca7..3d5f2336a0788e1554d7bf76c28ec7c1a727f335 100644
--- a/frontend/src/components/Login.vue
+++ b/frontend/src/components/Login.vue
@@ -26,7 +26,7 @@
                 type="password"
                 required
                 ></v-text-field>
-              <v-btn type="submit" color="primary">Access</v-btn>
+              <v-btn :loading="loading" type="submit" color="primary">Access</v-btn>
             </v-form>
           </v-flex>
         </v-layout>
@@ -35,6 +35,7 @@
 
 
 <script>
+  import {mapActions, mapState} from 'vuex'
   export default {
     name: 'grady-login',
     data () {
@@ -43,16 +44,30 @@
           username: '',
           password: ''
         },
-        error: ''
+        loading: false
       }
     },
+    computed: {
+      ...mapState([
+        'error'
+      ])
+    },
     methods: {
+      ...mapActions([
+        'getJWTToken',
+        'getExamModule',
+        'getUserRole',
+        'getJWTTimeDelta'
+      ]),
       submit () {
-        this.$store.dispatch('getJWTToken', this.credentials).then(response => {
-          this.$router.push('/reviewer/')
-        }).catch(_ => {
-          this.error = this.$store.state.error
-        })
+        this.loading = true
+        this.getJWTToken(this.credentials).then(() => {
+          this.loading = false
+          this.getExamModule()
+          this.getUserRole()
+          this.getJWTTimeDelta()
+          this.$router.push('/student/')
+        }).catch(() => { this.loading = false })
       }
     }
   }
diff --git a/frontend/src/components/base/BaseLayout.vue b/frontend/src/components/base/BaseLayout.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b3c9950c5ca538b33868c9895386df4fbfb0ae64
--- /dev/null
+++ b/frontend/src/components/base/BaseLayout.vue
@@ -0,0 +1,91 @@
+<template>
+  <div>
+    <v-navigation-drawer
+      fixed
+      clipped
+      app
+      permanent
+      :mini-variant.sync="mini"
+    >
+      <v-toolbar flat>
+        <v-list>
+          <v-list-tile>
+            <v-list-tile-action v-if="mini">
+              <v-btn icon @click.native.stop="mini = !mini">
+                <v-icon>chevron_right</v-icon>
+              </v-btn>
+            </v-list-tile-action>
+            <v-list-tile-content class="title">
+              {{ examInstance }}
+            </v-list-tile-content>
+            <v-list-tile-action>
+              <v-btn icon @click.native.stop="mini = !mini">
+                <v-icon>chevron_left</v-icon>
+              </v-btn>
+            </v-list-tile-action>
+          </v-list-tile>
+        </v-list>
+      </v-toolbar>
+        <slot name="navigation"></slot>
+    </v-navigation-drawer>
+    <v-toolbar
+      app
+      clipped-left
+      fixed
+      dark
+      color="indigo darken-4"
+      class="grady-toolbar"
+    >
+      <v-toolbar-title>
+        <v-avatar>
+          <img src="../../assets/brand.png">
+        </v-avatar>
+      </v-toolbar-title>
+      <span class="pl-2 grady-speak">{{ gradySpeak }}</span>
+      <div class="toolbar-content">
+        <span>{{ userRole }} | {{ username }}</span>
+      </div>
+      <v-btn color="blue darken-1" to="/" @click.native="logout">Logout</v-btn>
+    </v-toolbar>
+    <v-content>
+      <slot></slot>
+    </v-content>
+  </div>
+</template>
+
+<script>
+  import { mapActions, mapGetters, mapState } from 'vuex'
+  export default {
+    name: 'base-layout',
+    data () {
+      return {
+        mini: false
+      }
+    },
+    computed: {
+      ...mapGetters([
+        'gradySpeak'
+      ]),
+      ...mapState([
+        'examInstance',
+        'username',
+        'userRole'
+      ])
+    },
+    methods: {
+      ...mapActions([
+        'logout'
+      ])
+    }
+  }
+</script>
+
+<style scoped>
+  .toolbar-content {
+    margin-left: auto;
+  }
+
+  .grady-toolbar {
+    font-weight: bold;
+  }
+</style>
diff --git a/frontend/src/components/student/StudentLayout.vue b/frontend/src/components/student/StudentLayout.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bc313774717da683f1ba8a3fdac6eda1107546ab
--- /dev/null
+++ b/frontend/src/components/student/StudentLayout.vue
@@ -0,0 +1,62 @@
+<template>
+  <base-layout>
+    <v-list dense slot="navigation">
+      <v-list-tile exact v-for="(item, i) in generalNavItems" :key="i" :to="item.route">
+        <v-list-tile-action>
+          <v-icon>{{ item.icon }}</v-icon>
+        </v-list-tile-action>
+        <v-list-tile-content>
+          <v-list-tile-title>
+            {{ item.name }}
+          </v-list-tile-title>
+        </v-list-tile-content>
+      </v-list-tile>
+      <v-divider></v-divider>
+    <v-list-tile exact v-for="(item, i) in submissionNavItems" :key="i" :to="item.route">
+      <v-list-tile-action>
+        <v-icon>assignment</v-icon>
+      </v-list-tile-action>
+      <v-list-tile-content>
+        <v-list-tile-title>
+          {{ item.name }}
+        </v-list-tile-title>
+      </v-list-tile-content>
+    </v-list-tile>
+    </v-list>
+    <router-view></router-view>
+  </base-layout>
+</template>
+
+<script>
+  import BaseLayout from '../base/BaseLayout'
+  export default {
+    components: {BaseLayout},
+    name: 'student-layout',
+    data () {
+      return {
+        generalNavItems: [
+          {
+            name: 'Overview',
+            icon: 'home',
+            route: '/student/'
+          },
+          {
+            name: 'Statistics',
+            icon: 'show_chart',
+            route: '/student/'
+          }
+        ]
+      }
+    },
+    computed: {
+      submissionNavItems: function () {
+        return this.$store.state.studentPage.submissions.map((sub, index) => {
+          return {
+            name: sub.type,
+            route: `/student/submission/${index}`
+          }
+        })
+      }
+    }
+  }
+</script>
diff --git a/frontend/src/components/student/StudentNav.vue b/frontend/src/components/student/StudentNav.vue
deleted file mode 100644
index 3b2484e9d207c8f3e87b8ff9d67eaaec93a1e4d9..0000000000000000000000000000000000000000
--- a/frontend/src/components/student/StudentNav.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-  <v-navbar toggleable="md" type="light" variant="light">
-    <v-navbar-toggle target="nav_collapse"></v-navbar-toggle>
-
-    <v-navbar-brand>
-      <img src="../../assets/brand.png" width="30" class="d-inline-block align-top">
-      Grady
-    </v-navbar-brand>
-
-    <v-collapse is-nav id="nav_collapse">
-
-      <v-navbar-nav id="nav-left">
-        <v-nav-item class="active" href="#">Results</v-nav-item>
-        <v-nav-item href="#">Statistics</v-nav-item>
-      </v-navbar-nav>
-
-      <!-- Right aligned nav items -->
-      <v-navbar-nav class="ml-auto">
-        <v-nav-item>{{ this.$store.state.username }}</v-nav-item>
-        <router-link to="/">
-          <v-button class="btn-dark" @click="logout()" >Signout</v-button>
-        </router-link>
-      </v-navbar-nav>
-    </v-collapse>
-  </v-navbar>
-</template>
-
-
-<script>
-  export default {
-    name: 'grady-nav',
-    methods: {
-      logout () {
-        this.$store.dispatch('logout')
-      }
-    }
-  }
-</script>
diff --git a/frontend/src/components/student/StudentPage.vue b/frontend/src/components/student/StudentPage.vue
index b1e9249692fde918bbbfb6f48106301ed3fb62ef..eb1d85adf06ff1d24c008cbc2e7e8a905cc88761 100644
--- a/frontend/src/components/student/StudentPage.vue
+++ b/frontend/src/components/student/StudentPage.vue
@@ -1,25 +1,22 @@
 <template>
-  <div>
-    <grady-nav></grady-nav>
-    <div class="container-fluid">
-      <div class="row justify-content-center my-3">
-        <div class="col-md-3">
-          <h2 class="my-5">Exam Overview</h2>
+    <v-container fluid>
+      <v-layout justify center>
+        <v-flex md3>
+          <h2>Exam Overview</h2>
           <exam-information v-if="doneLoading" :exam="exam"></exam-information>
-        </div>
-        <div class="col-md-6 offset-md-1" v-if="doneLoading">
-          <h2 class="my-5">Submissions of {{ this.studentData.name }}</h2>
+        </v-flex>
+        <v-flex md7 offset-md1 v-if="doneLoading">
+          <h2>Submissions of {{ studentName }}</h2>
           <submission-list :submissions="submissions"></submission-list>
-        </div>
-      </div>
-    </div>
-  </div>
+        </v-flex>
+      </v-layout>
+    </v-container>
 </template>
 
 
 <script>
-  import ax from '@/store/api'
-  import GradyNav from './StudentNav.vue'
+  import {mapState} from 'vuex'
+  import StudentLayout from './StudentLayout.vue'
   import SubmissionList from './SubmissionList.vue'
   import ExamInformation from './ExamInformation.vue'
 
@@ -27,28 +24,22 @@
     components: {
       ExamInformation,
       SubmissionList,
-      GradyNav},
+      StudentLayout},
     name: 'student-page',
     data () {
       return {
-        studentData: {},
         doneLoading: false
       }
     },
     created: function () {
-      this.doneLoading = false
-      ax.get('api/student/').then(response => {
-        this.studentData = response.data
-        this.doneLoading = true
-      })
+      this.$store.dispatch('getStudentData').then(() => { this.doneLoading = true })
     },
     computed: {
-      submissions () {
-        return this.studentData.submissions
-      },
-      exam () {
-        return this.studentData.exam
-      }
+      ...mapState({
+        studentName: state => state.studentPage.studentName,
+        exam: state => state.studentPage.exam,
+        submissions: state => state.studentPage.submissions
+      })
     }
   }
 </script>
diff --git a/frontend/src/components/student/SubmissionDetail.vue b/frontend/src/components/student/SubmissionDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..156a973761b05d60fe61d8cd87be824f04e84153
--- /dev/null
+++ b/frontend/src/components/student/SubmissionDetail.vue
@@ -0,0 +1,17 @@
+<template>
+  <v-layout>
+
+    <annotated-submission class="ma-3"></annotated-submission>
+  </v-layout>
+</template>
+
+
+<script>
+  import AnnotatedSubmission from '../submission_notes/AnnotatedSubmission'
+  export default {
+    components: {
+      AnnotatedSubmission
+    },
+    name: 'submission-detail'
+  }
+</script>
diff --git a/frontend/src/components/student/SubmissionList.vue b/frontend/src/components/student/SubmissionList.vue
index c39e745fc152cca06a50428cc0f9fe595b858230..4db72e636c0a40abe094db94accb9b64ab4bfb9e 100644
--- a/frontend/src/components/student/SubmissionList.vue
+++ b/frontend/src/components/student/SubmissionList.vue
@@ -1,9 +1,20 @@
 <template>
   <div class="row my-2 justify-content-center">
-    <b-table hover :items="submissions" :fields="fields"></b-table>
-    <div class="alert alert-info">
-      You reached <b>{{ sumScore }}</b> of <b>{{ sumFullScore }}</b> possible points( {{ pointRatio }}% ).
-    </div>
+    <v-data-table
+      hide-actions
+      :headers="headers"
+      :items="submissions"
+    >
+      <template slot="items" slot-scope="props">
+        <td>{{ props.item.type }}</td>
+        <td class="text-xs-right">{{ props.item.score }}</td>
+        <td class="text-xs-right">{{ props.item.full_score }}</td>
+        <td class="text-xs-right"><v-btn :to="`submission/${props.index}`" color="red">View</v-btn></td>
+      </template>
+    </v-data-table>
+    <v-alert color="info" value="true">
+      You reached <b>{{ sumScore }}</b> of <b>{{ sumFullScore }}</b> possible points ( {{ pointRatio }}% ).
+    </v-alert>
   </div>
 </template>
 
@@ -13,6 +24,22 @@
     name: 'submission-list',
     data () {
       return {
+        headers: [
+          {
+            text: 'Task',
+            align: 'left',
+            value: 'type'
+          },
+          {
+            text: 'Score',
+            value: 'score'
+          },
+          {
+            text: 'Maximum Score',
+            value: 'full_score'
+          }
+        ],
+
         fields: [
           { key: 'type', sortable: true },
           { key: 'score', label: 'Score', sortable: true },
diff --git a/frontend/src/components/submission_notes/AnnotatedSubmission.vue b/frontend/src/components/submission_notes/AnnotatedSubmission.vue
new file mode 100644
index 0000000000000000000000000000000000000000..973078dab349056080ba95636747214a77b66799
--- /dev/null
+++ b/frontend/src/components/submission_notes/AnnotatedSubmission.vue
@@ -0,0 +1,103 @@
+<template>
+  <table>
+    <tr v-for="(code, index) in submission" :key="index">
+      <td class="line-number-cell">
+        <!--<v-tooltip left close-delay="20" color="transparent" content-class="comment-icon">-->
+          <v-btn block class="line-number-btn" slot="activator" @click="toggleEditorOnLine(index)">{{ index }}</v-btn>
+          <!--<v-icon small color="indigo accent-3" class="comment-icon">comment</v-icon>-->
+        <!--</v-tooltip>-->
+      </td>
+      <td>
+        <pre class="prettyprint"><code class="lang-c"> {{ code }}</code></pre>
+        <feedback-comment
+          v-if="feedback[index] && !showEditorOnLine[index]"
+          @click="toggleEditorOnLine(index)">{{ feedback[index] }}
+        </feedback-comment>
+        <comment-form
+          v-if="showEditorOnLine[index]"
+          @collapseFeedbackForm="showEditorOnLine[index] = false"
+          :feedback="feedback[index]"
+          :index="index">
+        </comment-form>
+      </td>
+    </tr>
+  </table>
+</template>
+
+
+<script>
+  import {mapGetters, mapState} from 'vuex'
+  import CommentForm from '@/components/submission_notes/FeedbackForm.vue'
+  import FeedbackComment from '@/components/submission_notes/FeedbackComment.vue'
+
+  export default {
+    components: {
+      FeedbackComment,
+      CommentForm},
+    name: 'annotated-submission',
+    beforeCreate () {
+      this.$store.dispatch('getFeedback', 0)
+      this.$store.dispatch('getSubmission', 0)
+    },
+    computed: {
+      ...mapState({
+        feedback: state => state.submissionNotes.feedback
+      }),
+      ...mapGetters(['submission'])
+    },
+    data: function () {
+      return {
+        showEditorOnLine: { }
+      }
+    },
+    methods: {
+      toggleEditorOnLine (lineIndex) {
+        this.$set(this.showEditorOnLine, lineIndex, !this.showEditorOnLine[lineIndex])
+      }
+    },
+    mounted () {
+      window.PR.prettyPrint()
+    }
+  }
+</script>
+
+
+<style scoped>
+
+  table {
+    table-layout: auto;
+    border-collapse: collapse;
+  }
+
+  td {
+    /*white-space: nowrap;*/
+    /*border: 1px solid green;*/
+  }
+
+  .line-number-cell {
+    /*padding-left: 50px;*/
+    vertical-align: top;
+  }
+
+  pre.prettyprint {
+    padding: 0;
+    border: 0;
+  }
+
+  code {
+    width: 100%;
+    box-shadow: None;
+  }
+
+
+  .line-number-btn {
+    height: fit-content;
+    min-width: fit-content;
+    margin: 0;
+  }
+
+  .comment-icon {
+    border: 0;
+  }
+
+</style>
diff --git a/frontend/src/components/submission_notes/FeedbackComment.vue b/frontend/src/components/submission_notes/FeedbackComment.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a63b4de6a32267608763b9f999f5088adf462e71
--- /dev/null
+++ b/frontend/src/components/submission_notes/FeedbackComment.vue
@@ -0,0 +1,54 @@
+<template>
+  <div class="dialogbox">
+    <div class="body">
+      <span class="tip tip-up"></span>
+      <div class="message">
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+</template>
+
+
+<script>
+  export default {
+    name: 'feedback-comment'
+  }
+</script>
+
+
+<style scoped>
+  .tip {
+    width: 0px;
+    height: 0px;
+    position: absolute;
+    background: transparent;
+    border: 10px solid #3D8FC1;
+  }
+
+  .tip-up {
+    top: -25px; /* Same as body margin top + border */
+    left: 10px;
+    border-right-color: transparent;
+    border-left-color: transparent;
+    border-top-color: transparent;
+  }
+
+  .dialogbox .body {
+    position: relative;
+    height: auto;
+    margin: 20px 10px 10px 10px;
+    padding: 5px;
+    background-color: #F3F3F3;
+    border-radius: 5px;
+    border: 5px solid #3D8FC1;
+  }
+
+  .body .message {
+    font-family: Roboto, sans-serif;
+    min-height: 30px;
+    border-radius: 3px;
+    font-size: 14px;
+    line-height: 1.5;
+  }
+</style>
diff --git a/frontend/src/components/submission_notes/FeedbackForm.vue b/frontend/src/components/submission_notes/FeedbackForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7813c0be9323b110ad7a19518de65c1f35a7fa60
--- /dev/null
+++ b/frontend/src/components/submission_notes/FeedbackForm.vue
@@ -0,0 +1,54 @@
+<template>
+  <div>
+    <v-text-field
+      name="feedback-input"
+      label="Please provide your feedback here"
+      v-model="current_feedback"
+      @keyup.enter.ctrl.exact="submitFeedback"
+      @keyup.esc="collapseTextField"
+      rows="2"
+      textarea
+      autofocus
+      auto-grow
+      hide-details
+    ></v-text-field>
+    <v-btn color="success" @click="submitFeedback">Submit</v-btn>
+    <v-btn @click="discardFeedback">Discard changes</v-btn>
+  </div>
+</template>
+
+
+<script>
+  export default {
+    name: 'comment-form',
+    props: ['feedback', 'index'],
+    data () {
+      return {
+        current_feedback: this.feedback
+      }
+    },
+    methods: {
+
+      collapseTextField () {
+        this.$emit('collapseFeedbackForm')
+      },
+      submitFeedback () {
+        this.$store.dispatch('updateFeedback', {
+          lineIndex: this.index,
+          content: this.current_feedback
+        })
+        this.collapseTextField()
+      },
+      discardFeedback () {
+        this.current_feedback = this.feedback
+      }
+    }
+  }
+</script>
+
+
+<style scoped>
+  v-text-field {
+    padding-top: 0px;
+  }
+</style>
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 997422b82770d3b595a39127ea98f36215f9c98c..d7cf28b7ac93b39520f3c04233df92b88fc04f8e 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -7,6 +7,9 @@ import store from './store/store'
 import Vuetify from 'vuetify'
 
 import 'vuetify/dist/vuetify.min.css'
+import 'material-design-icons/iconfont/material-icons.css'
+import 'google-code-prettify/bin/prettify.min'
+import 'google-code-prettify/bin/prettify.min.css'
 
 Vue.use(Vuetify)
 
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index efb8236906d71e2dc99b1f2f5453851413f6988a..849fbf66c4d47a38992f6ea97e5dcf25776d7966 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -1,13 +1,18 @@
 import Vue from 'vue'
 import Router from 'vue-router'
+import store from '../store/store'
 import Login from '@/components/Login'
 import StudentPage from '@/components/student/StudentPage'
+import StudentLayout from '@/components/student/StudentLayout'
+import SubmissionDetail from '@/components/student/SubmissionDetail'
 import ReviewerPage from '@/components/reviewer/ReviewerPage'
 import StudentListOverview from '@/components/reviewer/StudentListOverview'
+import BaseLayout from '@/components/base/BaseLayout'
+import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission'
 
 Vue.use(Router)
 
-export default new Router({
+const router = new Router({
   routes: [
     {
       path: '/',
@@ -16,8 +21,18 @@ export default new Router({
     },
     {
       path: '/student/',
-      name: 'student-page',
-      component: StudentPage
+      component: StudentLayout,
+      children: [
+        {
+          path: '',
+          component: StudentPage
+        },
+        {
+          path: 'submission/:id',
+          component: SubmissionDetail
+        }
+      ]
+
     },
     {
       path: '/reviewer/',
@@ -28,6 +43,37 @@ export default new Router({
       path: 'reviewer/student-overview/',
       name: 'student-overview',
       component: StudentListOverview
+    },
+    {
+      path: '/base/',
+      name: 'base-layout',
+      component: BaseLayout
+    },
+    {
+      path: '/notes/',
+      name: 'annotated-submission',
+      component: AnnotatedSubmission
     }
   ]
 })
+
+router.beforeEach((to, from, next) => {
+  if (to.path === '/') {
+    next()
+  } else {
+    const now = new Date()
+    if (now - store.state.logInTime > store.state.jwtTimeDelta * 1000) {
+      console.log(now)
+      console.log(store.state.logInTime)
+      store.dispatch('logout').then(() => {
+        store.commit('API_FAIL', 'You\'ve been logged out due to inactivity')
+        next('/')
+      })
+    } else {
+      store.dispatch('refreshJWTToken')
+      next()
+    }
+  }
+})
+
+export default router
diff --git a/frontend/src/store/api.js b/frontend/src/store/api.js
index 368c09c86195ded2f5c97b1199a26e37c4184190..c1e523645706a4f2c07c2282906e138186c67d4f 100644
--- a/frontend/src/store/api.js
+++ b/frontend/src/store/api.js
@@ -1,7 +1,8 @@
 import axios from 'axios'
 
-var ax = axios.create({
-  baseURL: 'http://localhost:8000/'
+let ax = axios.create({
+  baseURL: 'http://localhost:8000/',
+  headers: {'Authorization': 'JWT ' + sessionStorage.getItem('jwtToken')}
 })
 
 export default ax
diff --git a/frontend/src/store/grady_speak.js b/frontend/src/store/grady_speak.js
new file mode 100644
index 0000000000000000000000000000000000000000..6ae7abba064d4c3f07922ad004073bea8baacbfc
--- /dev/null
+++ b/frontend/src/store/grady_speak.js
@@ -0,0 +1,25 @@
+const gradySays = [
+  'Now let\'s see if we can improve this with a little water, sir.',
+  'Won\'t keep you a moment, sir.',
+  'Grady, sir. Delbert Grady.',
+  'Yes, sir.',
+  'That\'s right, sir.',
+  'Why no, sir. I don\'t believe so.',
+  'Ah ha, it\'s coming off now, sir.',
+  'Why no, sir. I don\'t believe so.',
+  'Yes, sir.  I have a wife and two daughters, sir.',
+  'Oh, they\'re somewhere around.  I\'m not quite sure at the moment, sir.',
+  'That\'s strange, sir.  I don\'t have any recollection of that at all.',
+  'I\'m sorry to differ with you, sir, but you are the caretaker.',
+  'You have always been the caretaker, I should know, sir.',
+  'I\'ve always been here.',
+  'Indeed, he is, Mr. Torrance. A very willful boy. ',
+  'A rather naughty boy, if I may be so bold, sir.',
+  'Perhaps they need a good talking to, if you don\'t mind my saying so. Perhaps a bit more.',
+  'My girls, sir, they didn\'t care for the Overlook at first.',
+  'One of them actually stole a packet of matches and tried to burn it down.',
+  'But I corrected them, sir.',
+  'And when my wife tried to prevent me from doing my duty... I corrected her.'
+]
+
+export default gradySays
diff --git a/frontend/src/store/modules/student-page.js b/frontend/src/store/modules/student-page.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d593808f718bc8b4814d0789546439c7950619c
--- /dev/null
+++ b/frontend/src/store/modules/student-page.js
@@ -0,0 +1,36 @@
+import ax from '../api'
+
+const studentPage = {
+  state: {
+    studentName: '',
+    exam: {},
+    submissionTypes: [],
+    submissions: []
+  },
+  mutations: {
+    'SET_STUDENT_NAME': function (state, name) {
+      state.studentName = name
+    },
+    'SET_EXAM': function (state, exam) {
+      state.exam = exam
+    },
+    'SET_SUBMISSION_TYPES': function (state, submissionTypes) {
+      state.submissionTypes = submissionTypes
+    },
+    'SET_SUBMISSIONS': function (state, submissions) {
+      state.submissions = submissions
+    }
+  },
+  actions: {
+    getStudentData (context) {
+      ax.get('api/student-page/').then(response => {
+        const data = response.data
+        context.commit('SET_STUDENT_NAME', data.name)
+        context.commit('SET_EXAM', data.exam)
+        context.commit('SET_SUBMISSIONS', data.submissions)
+      })
+    }
+  }
+}
+
+export default studentPage
diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.js
new file mode 100644
index 0000000000000000000000000000000000000000..cde09fa0c39e2f4a38869dc8746aee0c3be86628
--- /dev/null
+++ b/frontend/src/store/modules/submission-notes.js
@@ -0,0 +1,76 @@
+import Vue from 'vue'
+
+const mockSubmission = '//Procedural Programming technique shows creation of Pascal\'s Triangl\n' +
+  '#include <iostream>\n' +
+  '#include <iomanip>\n' +
+  'using namespace std;\n' +
+  'int** comb(int** a , int row , int col)\n' +
+  '{\n' +
+  '   int mid = col/2;\n' +
+  '        //clear matrix\n' +
+  '         for( int i = 0 ; i < row ; i++)\n' +
+  '         for( int j = 0 ; j < col ; j++)\n' +
+  '                a[i][j] = 0;\n' +
+  '                a[0][mid] = 1; //put 1 in the middle of first row\n' +
+  '    //build up Pascal\'s Triangle matrix\n' +
+  '     for( int i = 1 ; i < row ; i++)\n' +
+  '        {\n' +
+  '          for( int j = 1 ; j < col - 1 ; j++)\n' +
+  '               a[i][j] = a[i-1][j-1] + a[i-1][j+1];\n' +
+  '        }\n' +
+  '   return a;\n' +
+  '}\n' +
+  'void disp(int** ptr, int row, int col)\n' +
+  '{\n' +
+  '  cout << endl << endl;\n' +
+  '    for ( int i = 0 ; i < row ; i++)\n' +
+  '        {\n' +
+  '        for ( int j = 0 ; j < col ; j++)\n'
+
+const mockFeedback = {
+  '1': 'Youre STUPID',
+  '4': 'Very much so'
+}
+
+const submissionNotes = {
+  state: {
+    rawSubmission: '',
+    feedback: {}
+  },
+  getters: {
+    // reduce the string rawSubmission into an object where the keys are the
+    // line indexes starting at one and the values the corresponding submission line
+    // this makes iterating over the submission much more pleasant
+    submission: state => {
+      return state.rawSubmission.split('\n').reduce((acc, cur, index) => {
+        acc[index + 1] = cur
+        return acc
+      }, {})
+    }
+  },
+  mutations: {
+    'SET_RAW_SUBMISSION': function (state, submission) {
+      state.rawSubmission = mockSubmission
+    },
+    'SET_FEEDBACK': function (state, feedback) {
+      state.feedback = feedback
+    },
+    'UPDATE_FEEDBACK': function (state, feedback) {
+      Vue.set(state.feedback, feedback.lineIndex, feedback.content)
+    }
+  },
+  actions: {
+    // TODO remove mock data
+    getSubmission (context, submissionId) {
+      context.commit('SET_RAW_SUBMISSION', mockSubmission)
+    },
+    getFeedback (context, feedbackId) {
+      context.commit('SET_FEEDBACK', mockFeedback)
+    },
+    updateFeedback (context, lineIndex, feedbackContent) {
+      context.commit('UPDATE_FEEDBACK', lineIndex, feedbackContent)
+    }
+  }
+}
+
+export default submissionNotes
diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js
index 3eb032bf8effaf4fb5e4e2e69237d1c74fb588b7..7d99d56b21ba722b6ed7343127b593cc7d88c29a 100644
--- a/frontend/src/store/store.js
+++ b/frontend/src/store/store.js
@@ -2,37 +2,69 @@ import Vuex from 'vuex'
 import Vue from 'vue'
 import ax from './api'
 
+import gradySays from './grady_speak'
+import submissionNotes from './modules/submission-notes'
+import studentPage from './modules/student-page'
+
 Vue.use(Vuex)
 
 const store = new Vuex.Store({
+  modules: {
+    submissionNotes,
+    studentPage
+  },
   state: {
-    token: '',
-    loggedIn: false,
-    username: '',
-    error: ''
+    token: sessionStorage.getItem('jwtToken'),
+    loggedIn: !!sessionStorage.getItem('jwtToken'),
+    logInTime: sessionStorage.getItem('logInTime'),
+    username: sessionStorage.getItem('username'),
+    jwtTimeDelta: sessionStorage.getItem('jwtTimeDelta'),
+    userRole: sessionStorage.getItem('userRole'),
+    error: '',
+    examInstance: ''
+  },
+  getters: {
+    gradySpeak: () => {
+      return gradySays[Math.floor(Math.random() * gradySays.length)]
+    }
   },
   mutations: {
     'API_FAIL': function (state, error) {
       state.error = error
     },
-    'LOGIN': function (state, creds) {
-      state.token = creds.token
+    'SET_JWT_TOKEN': function (state, token) {
+      state.token = token
+      state.logInTime = Date.now()
+      ax.defaults.headers['Authorization'] = 'JWT ' + token
+      sessionStorage.setItem('jwtToken', token)
+      sessionStorage.setItem('logInTime', state.logInTime)
+    },
+    'SET_JWT_TIME_DELTA': function (state, timeDelta) {
+      state.jwtTimeDelta = timeDelta
+      sessionStorage.setItem('jwtTimeDelta', timeDelta)
+    },
+    'LOGIN': function (state, username) {
       state.loggedIn = true
-      state.username = creds.username
+      state.username = username
+      sessionStorage.setItem('username', username)
     },
     'LOGOUT': function (state) {
-      state.token = ''
       state.loggedIn = false
+    },
+    'SET_USER_ROLE': function (state, userRole) {
+      state.userRole = userRole
+      sessionStorage.setItem('userRole', userRole)
+    },
+    'SET_EXAM_INSTANCE': function (state, examInstance) {
+      state.examInstance = examInstance
     }
   },
   actions: {
     async getJWTToken (context, credentials) {
       try {
         const response = await ax.post('api-token-auth/', credentials)
-        context.commit('LOGIN', {
-          token: response.data.token,
-          username: credentials.username
-        })
+        context.commit('LOGIN', credentials.username)
+        context.commit('SET_JWT_TOKEN', response.data.token)
       } catch (error) {
         if (error.response) {
           const errorMsg = 'Unable to log in with provided credentials.'
@@ -45,8 +77,25 @@ const store = new Vuex.Store({
         }
       }
     },
+    refreshJWTToken (context) {
+      ax.post('/api-token-refresh/', {token: context.state.token}).then(response => {
+        context.commit('SET_JWT_TOKEN', response.data.token)
+      })
+    },
+    getJWTTimeDelta (context) {
+      ax.get('api/jwt-time-delta/').then(response => {
+        context.commit('SET_JWT_TIME_DELTA', response.data.timeDelta)
+      })
+    },
+    getUserRole (context) {
+      ax.get('api/user-role/').then(response => context.commit('SET_USER_ROLE', response.data.role))
+    },
+    getExamModule (context) {
+      ax.get('api/exam-module/').then(response => context.commit('SET_EXAM_INSTANCE', response.data.exam))
+    },
     logout (store) {
       store.commit('LOGOUT')
+      store.commit('SET_JWT_TOKEN', '')
     }
   }
 })
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 7fac4471d3aef38b2a829c2ace79c724663298f8..1e43569c96644d2375e6b23a0c94ead389528e24 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -228,6 +228,12 @@ async@1.x, async@^1.4.0, async@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
+async@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
+  dependencies:
+    lodash "^4.14.0"
+
 async@^2.1.2, async@^2.4.1:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
@@ -1355,6 +1361,13 @@ component-inherit@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
 
+compression-webpack-plugin@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.0.1.tgz#7f0a2af9f642b4f87b5989516a3b9e9b41bb4b3f"
+  dependencies:
+    async "2.4.1"
+    webpack-sources "^1.0.1"
+
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2682,6 +2695,10 @@ globby@^5.0.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
+google-code-prettify@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/google-code-prettify/-/google-code-prettify-1.0.5.tgz#9f477f224dbfa62372e5ef803a7e157410400084"
+
 graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -3704,6 +3721,10 @@ map-obj@^1.0.0, map-obj@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
 
+material-design-icons@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf"
+
 math-expression-evaluator@^1.2.14:
   version "1.2.17"
   resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"