diff --git a/index.html b/index.html
index 6d524eece8886bfeff1e9b795f5a2d5e25162e2a..bf12928aeae431bc02f3bcf4d39cbce201a60571 100644
--- a/index.html
+++ b/index.html
@@ -1,139 +1,17 @@
-<!DOCTYPE html>
-<html lang="en">
+<!doctype html>
+<html>
 
 <head>
-	<meta charset="utf-8">
-	<meta http-equiv="X-UA-Compatible" content="IE=edge">
-	<meta name="viewport" content="width=device-width, initial-scale=1">
-	<title>Admintool</title>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>webpack-babel-eslint</title>
 
-	<link rel="apple-touch-icon" sizes="57x57" href="res/favicon/apple-icon-57x57.png">
-	<link rel="apple-touch-icon" sizes="60x60" href="res/favicon/apple-icon-60x60.png">
-	<link rel="apple-touch-icon" sizes="72x72" href="res/favicon/apple-icon-72x72.png">
-	<link rel="apple-touch-icon" sizes="76x76" href="res/favicon/apple-icon-76x76.png">
-	<link rel="apple-touch-icon" sizes="114x114" href="res/favicon/apple-icon-114x114.png">
-	<link rel="apple-touch-icon" sizes="120x120" href="res/favicon/apple-icon-120x120.png">
-	<link rel="apple-touch-icon" sizes="144x144" href="res/favicon/apple-icon-144x144.png">
-	<link rel="apple-touch-icon" sizes="152x152" href="res/favicon/apple-icon-152x152.png">
-	<link rel="apple-touch-icon" sizes="180x180" href="res/favicon/apple-icon-180x180.png">
-	<link rel="icon" type="image/png" sizes="192x192" href="res/favicon/android-icon-192x192.png">
-	<link rel="icon" type="image/png" sizes="32x32" href="res/favicon/favicon-32x32.png">
-	<link rel="icon" type="image/png" sizes="96x96" href="res/favicon/favicon-96x96.png">
-	<link rel="icon" type="image/png" sizes="16x16" href="res/favicon/favicon-16x16.png">
-	<link rel="manifest" href="res/favicon/manifest.json">
-	<meta name="msapplication-TileColor" content="#ffffff">
-	<meta name="msapplication-TileImage" content="res/favicon/ms-icon-144x144.png">
-	<meta name="theme-color" content="#ffffff">
-
-	<link href="lib/bootstrap/css/bootstrap.min.css" rel="stylesheet">
-	<link href="lib/cust/main.css" rel="stylesheet">
-
-	<script src="lib/jquery/jquery-2.2.2.min.js"></script>
-	<script src="lib/bootstrap/js/bootstrap.min.js"></script>
-	<script>
-	  // set the api url for the amivcore js library
-	  var api_url_config = "https://amiv-api.ethz.ch";
-	  var spec_url_config = "lib/amiv/spec.json";
-	</script>
-	<script src="lib/amiv/amivcore.js"></script>
-	<script src="lib/cust/main.js"></script>
-
-	<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/moment-with-locales.js"></script>
-	<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.14.30/js/bootstrap-datetimepicker.min.js"></script>
-	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.14.30/css/bootstrap-datetimepicker.css">
+    <link href="res/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+    <link href="lib/cust/main.css" rel="stylesheet">
 </head>
 
 <body>
-	<!-- Login Overlay-->
-	<div class="loginPanel smooth">
-		<div class="col-sm-4"></div>
-		<div class="col-sm-4 well">
-			<img class="login-logo" src="res/logo/main.svg" style="margin-left: 15%;" alt="AMIV Logo">
-			<div class="input-group">
-				<input type="text" class="form-control" id="loginUsername" name="user" placeholder="user">
-				<span class="input-group-addon">@student.ethz.ch</span>
-			</div>
-			<br>
-			<input type="password" class="form-control" id="loginPassword" name="password" placeholder="password">
-			<br>
-			<button type="submit" class="btn btn-default loginAction">Submit</button>
-		</div>
-		<div class="col-sm-4"></div>
-	</div>
-
-	<!-- Modal -->
-	<div class="modal fade modalCont" tabindex="-1" role="dialog">
-		<div class="modal-dialog">
-			<div class="modal-content">
-				<div class="modal-header">
-					<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-					<h4 class="modal-title"></h4>
-				</div>
-				<div class="modal-body"></div>
-				<div class="modal-footer">
-				</div>
-			</div>
-		</div>
-	</div>
-
-	<!-- Log Container -->
-	<div class="alertCont"></div>
-
-	<!-- Main Container -->
-	<div class="wrapper-main smooth">
-
-		<!-- Sidebar Container -->
-		<div class="wrapper-sidebar smooth">
-			<div class="container-fluid">
-				<a href="#home"><img class="sidebar-logo" src="res/logo/main.svg" alt="AMIV Logo"></a>
-				<ul class="nav nav-pills nav-stacked nav-sidebar" role="navigation">
-					<li>
-						<div class="input-group">
-							<input type="text" class="form-control" placeholder="Find Me!">
-							<span class="input-group-btn">
-								<button class="btn btn-default" type="button"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button>
-							</span>
-						</div>
-					</li>
-					<li role="separator" class="divider"></li>
-					<li><a href="#users"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> Users</a></li>
-					<li><a href="#events"><span class="glyphicon glyphicon-calendar" aria-hidden="true"></span> Events</a></li>
-					<li><a href="#groups"><span class="glyphicon glyphicon-blackboard" aria-hidden="true"></span> Groups</a></li>
-					<li><a href="#annouce"><span class="glyphicon glyphicon-bullhorn" aria-hidden="true"></span> Announce</a></li>
-					<li><a href="#studydocuments"><span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span> Studydocs</a></li>
-					<li><a href="#beerncoffee"><span class="glyphicon glyphicon-cutlery" aria-hidden="true"></span> Beer & Coffee</a></li>
-</li>
-				</ul>
-			</div>
-		</div>
-
-		<!-- Top Navbar -->
-		<nav class="navbar navbar-default navbar-main">
-			<div class="container-fluid">
-				<div class="navbar-header">
-					<button type="button" class="navbar-toggle collapsed toggleSidebarBtn" aria-expanded="false">
-						<span class="sr-only">Toggle navigation</span>
-						<span class="icon-bar"></span>
-						<span class="icon-bar"></span>
-						<span class="icon-bar"></span>
-					</button>
-					<img height="40" style="margin: 5px;" src="res/logo/wheel.svg" id="wheel-logo" class="smooth" alt="Loading Wheel">
-				</div>
-
-				<ul class="nav navbar-nav navbar-left cust-menu pull-left">
-				</ul>
-
-				<ul class="nav navbar-nav navbar-right pull-right">
-					<li><a href="#" class="logoutAction"><span class="glyphicon glyphicon-off" aria-hidden="true"></span></a></li>
-				</ul>
-			</div>
-		</nav>
-
-		<!-- Main Content / Tool -->
-		<div class="wrapper-content" id="main-content">
-		</div>
-	</div>
-
+    <script src="/dist/bundle.js"></script>
 </body>
 
 </html>
diff --git a/src/auth.js b/src/auth.js
new file mode 100644
index 0000000000000000000000000000000000000000..35e5e6d6ede18c134f93ae3e087d5a0dc83705eb
--- /dev/null
+++ b/src/auth.js
@@ -0,0 +1,88 @@
+import axios from 'axios';
+import * as localStorage from './localStorage';
+
+// Object which stores the current login-state
+const APISession = {
+  authenticated: false,
+  token: '',
+};
+
+const amivapi = axios.create({
+  baseURL: 'https://amiv-api.ethz.ch/',
+  timeout: 10000,
+  headers: { 'Content-Type': 'application/json' },
+});
+
+function checkToken(token) {
+  // check if a token is still valid
+  return new Promise((resolve, reject) => {
+    amivapi.get('users', {
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: token,
+      },
+    }).then((response) => {
+      console.log(response.data);
+      if (response.status === 200) resolve();
+      else reject();
+    }).catch(reject);
+  });
+}
+
+export function checkAuthenticated() {
+  // return a promise that resolves always, with a bool that shows whether
+  // the user is authenticated
+  return new Promise((resolve, reject) => {
+    if (APISession.authenticated) resolve(true);
+    else {
+      console.log('looking for token');
+      // let's see if we have a stored token
+      const token = localStorage.get('token');
+      console.log(`found this token: ${token}`);
+      if (token !== '') {
+        // check of token is valid
+        checkToken(token).then(() => {
+          APISession.token = token;
+          APISession.authenticated = true;
+          resolve(true);
+        }).catch(() => {
+          resolve(false);
+        });
+      } else resolve(false);
+    }
+  });
+}
+
+export function getSession() {
+  // Promise resolves with authenticated axios-session or fails
+  return new Promise((resolve, reject) => {
+    checkAuthenticated().then((authenticated) => {
+      if (authenticated) {
+        const authenticatedSession = axios.create({
+          baseURL: 'https://amiv-api.ethz.ch/',
+          timeout: 10000,
+          headers: {
+            'Content-Type': 'application/json',
+            Authorization: APISession.token,
+          },
+        });
+        resolve(authenticatedSession);
+      } else reject();
+    }).catch(reject);
+  });
+}
+
+export function login(username, password) {
+  return new Promise((resolve, reject) => {
+    amivapi.post('sessions', { username, password })
+      .then((response) => {
+        if (response.status === 201) {
+          APISession.token = response.data.token;
+          APISession.authenticated = true;
+          localStorage.set('token', response.data.token);
+          resolve();
+        }
+        reject();
+      }).catch(reject);
+  });
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a3e9886d68925d242d9bc9a9d759adbde4182d6
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,39 @@
+import { LoginScreen } from './login';
+import TableView from './views/tableView';
+import UserModal from './userTool';
+
+const m = require('mithril');
+
+const main = document.createElement('div');
+document.body.appendChild(main);
+const root = main;
+
+
+m.route(root, '/users', {
+  '/users': {
+    view() {
+      return m(TableView, {
+        resource: 'users',
+        keys: ['firstname', 'lastname', 'nethz', 'legi', 'membership'],
+      });
+    },
+  },
+  '/users/:id': UserModal,
+  '/events': {
+    view() {
+      return m(TableView, {
+        resource: 'events',
+        keys: ['title_de', 'time_start', 'show_website', 'spots', 'signup_count'],
+      });
+    },
+  },
+  '/groups': {
+    view() {
+      return m(TableView, {
+        resource: 'groups',
+        keys: ['name'],
+      });
+    },
+  },
+  '/login': LoginScreen,
+});
diff --git a/src/localStorage.js b/src/localStorage.js
new file mode 100644
index 0000000000000000000000000000000000000000..ddb35c3ad120a3e49e8a430e3d7ebb45f4bc1028
--- /dev/null
+++ b/src/localStorage.js
@@ -0,0 +1,36 @@
+// Get something stored at key from local storage
+export function get(key) {
+  const longStorage = window.sessionStorage.getItem(`glob-${key}`);
+  if (!longStorage || longStorage === '') {
+    // If longStorage is empty, look in short storage
+    return window.localStorage.getItem(`glob-${key}`);
+  }
+  return longStorage;
+}
+
+/**
+ * Remove variable in localStorage
+ * @param {string} cname
+ */
+export function remove(key) {
+  if (window.sessionStorage.getItem(`glob-${key}`)) {
+    window.sessionStorage.removeItem(`glob-${key}`);
+  }
+  if (window.localStorage.getItem(`glob-${key}`)) {
+    window.localStorage.removeItem(`glob-${key}`);
+  }
+}
+
+/**
+ * Save and get into localStorage
+ * @constructor
+ * @param {string} key
+ * @param {string} value
+ */
+export function set(key, value, shortSession = false) {
+  if (shortSession) {
+    window.sessionStorage.setItem(`glob-${key}`, value);
+  } else {
+    window.localStorage.setItem(`glob-${key}`, value);
+  }
+}
diff --git a/src/login.js b/src/login.js
new file mode 100644
index 0000000000000000000000000000000000000000..ca1a3409e8dbfdcde4aaf94f84606b4fd19725db
--- /dev/null
+++ b/src/login.js
@@ -0,0 +1,47 @@
+import { login } from './auth';
+
+const m = require('mithril');
+
+const FormState = {
+  username: '',
+  setUsername(v) { FormState.username = v; },
+  password: '',
+  setPassword(v) { FormState.password = v; },
+};
+
+export default class LoginScreen {
+  view() {
+    return m('div', { class: 'loginPanel smooth' }, [
+      m('div.col-sm-4'),
+      m('div.col-sm-4', [
+        m('img.login-logo', { src: 'res/logo/main.svg' }),
+        m('div.input-group', [
+          m('input.form-control', {
+            oninput: m.withAttr('value', FormState.setUsername),
+            placeholder: 'user',
+          }),
+          m('span.input-group-addon', '@student.ethz.ch'),
+        ]),
+        m('br'),
+        m('input.form-control', {
+          oninput: m.withAttr('value', FormState.setPassword),
+          placeholder: 'password',
+          type: 'password',
+        }),
+        m('br'),
+        m('button.btn.btn-default', {
+          onclick() {
+            login(FormState.username, FormState.password).then(() => {
+              m.route.set('/users');
+            });
+          },
+        }, 'Submit'),
+      ]),
+      m('div.col-sm-4'),
+      m('div', [
+        m('span', FormState.username),
+        m('span', FormState.password),
+      ]),
+    ]);
+  }
+}
diff --git a/src/userTool.js b/src/userTool.js
new file mode 100644
index 0000000000000000000000000000000000000000..54dc2d3004db2c9c5e3e52d86a70d180ac5a996d
--- /dev/null
+++ b/src/userTool.js
@@ -0,0 +1,133 @@
+import { ItemView } from './views/itemView';
+import { EditView, inputGroup, selectGroup } from './views/editView';
+import Table from './views/tableView';
+
+const m = require('mithril');
+
+const keyDescriptors = {
+  legi: 'Legi Number',
+  firstname: 'First Name',
+  lastname: 'Last Name',
+  rfid: 'RFID',
+  phone: 'Phone',
+  nethz: 'nethz Account',
+  gender: 'Gender',
+  department: 'Department',
+  email: 'Email',
+};
+
+class UserView extends ItemView {
+  constructor() {
+    super('users');
+    this.memberships = [];
+  }
+
+  view() {
+    // do not render anything if there is no data yet
+    if (!this.data) return m.trust('');
+
+    let membershipBadge = m('span.label.label-important', 'No Member');
+    if (this.data.membership === 'regular') {
+      membershipBadge = m('span.label.label-success', 'Member');
+    } else if (this.data.membership === 'extraordinary') {
+      membershipBadge = m('span.label.label-success', 'Extraordinary Member');
+    } else if (this.data.membership === 'honory') {
+      membershipBadge = m('span.label.label-warning', 'Honory Member');
+    }
+
+    const detailKeys = [
+      'email', 'phone', 'nethz', 'legi', 'rfid', 'department', 'gender'];
+
+    return m('div', [
+      m('h1', `${this.data.firstname} ${this.data.lastname}`),
+      membershipBadge,
+      m('table', detailKeys.map(key => m('tr', [
+        m('td.detail-descriptor', keyDescriptors[key]),
+        m('td', this.data[key] ? this.data[key] : ''),
+      ]))),
+      m('h2', 'Memberships'), m('br'),
+      m(Table, {
+        resource: 'groupmemberships',
+        keys: ['group.name', 'expiry'],
+        querystring: m.buildQueryString({
+          where: `user=="${this.id}"`,
+          embedded: '{"group":1}',
+        }),
+      }),
+      m('h2', 'Signups'), m('br'),
+      m(Table, {
+        resource: 'eventsignups',
+        keys: ['event.title_de'],
+        querystring: m.buildQueryString({
+          where: `user=="${this.id}"`,
+          embedded: '{"event":1}',
+        }),
+      }),
+    ]);
+  }
+}
+
+class UserEdit extends EditView {
+  constructor(vnode) {
+    super(vnode, 'users');
+  }
+
+  view() {
+    // do not render anything if there is no data yet
+    if (!this.data) return m.trust('');
+
+    // UPDATE button is inactive if form is not valid
+    const buttonArgs = this.patchOnClick([
+      'lastname', 'firstname', 'email', 'membership', 'gender']);
+    const updateButton = m(
+      'div.btn.btn-warning',
+      this.valid ? buttonArgs : { disabled: 'disabled' },
+      'Update',
+    );
+
+    return m('form', [
+      m('div.row', [
+        m(inputGroup, this.bind({
+          classes: 'col-xs-6', title: 'Last Name', name: 'lastname',
+        })),
+        m(inputGroup, this.bind({
+          classes: 'col-xs-6', title: 'First Name', name: 'firstname',
+        })),
+        m(inputGroup, this.bind({ title: 'Email', name: 'email' })),
+        m(selectGroup, this.bind({
+          classes: 'col-xs-6',
+          title: 'Membership Status',
+          name: 'membership',
+          options: ['regular', 'extraordinary', 'honory'],
+        })),
+        m(selectGroup, this.bind({
+          classes: 'col-xs-6',
+          title: 'Gender',
+          name: 'gender',
+          options: ['male', 'female'],
+        })),
+      ]),
+      m('span', JSON.stringify(this.data)),
+      m('span', JSON.stringify(this.errorLists)),
+      updateButton,
+    ]);
+  }
+}
+
+export default class UserModal {
+  constructor() {
+    this.edit = false;
+  }
+
+  view() {
+    if (this.edit) {
+      return m(UserEdit, { onfinish: () => { this.edit = false; m.redraw(); } });
+    }
+    // else
+    return m('div', [
+      m('div.btn', { onclick: () => { this.edit = true; } }, 'Edit'),
+      m('br'),
+      m(UserView),
+    ]);
+  }
+}
diff --git a/src/views/editView.js b/src/views/editView.js
new file mode 100644
index 0000000000000000000000000000000000000000..46d07d7f4c0dd6c40dffd7151e405481903ebe24
--- /dev/null
+++ b/src/views/editView.js
@@ -0,0 +1,138 @@
+import Ajv from 'ajv';
+import { ItemView } from './itemView';
+import { getSession } from '../auth';
+
+
+const m = require('mithril');
+
+const objectNameForResource = {
+  users: 'User',
+};
+
+export class EditView extends ItemView {
+  constructor(vnode, resource, valid = true) {
+    super(resource);
+    this.changed = false;
+
+    // state for validation
+    this.valid = valid;
+    this.ajv = new Ajv({ missingRefs: 'ignore' });
+    this.errors = {};
+
+    // callback when edit is finished
+    this.callback = vnode.attrs.onfinish;
+  }
+
+  oninit() {
+    // load data for item
+    getSession().then((apiSession) => {
+      this.loadItemData(apiSession);
+    }).catch(() => {
+      m.route.set('/login');
+    });
+    // load schema
+    m.request('http://amiv-api.ethz.ch/docs/api-docs').then((schema) => {
+      const objectSchema = schema.definitions[
+        objectNameForResource[this.resource]];
+      this.ajv.addSchema(objectSchema, 'schema');
+    });
+  }
+
+  // bind form-fields to the object data and validation
+  bind(attrs) {
+    // initialize error-list for every bound field
+    if (!this.errors[attrs.name]) this.errors[attrs.name] = [];
+
+    const boundFormelement = {
+      onchange: (e) => {
+        this.changed = true;
+        // bind changed data
+        this.data[e.target.name] = e.target.value;
+
+        // validate against schema
+        const validate = this.ajv.getSchema('schema');
+        this.valid = validate(this.data);
+
+        // get errors of this field
+        let errors = [];
+        if (!this.valid) {
+          errors = validate.errors.filter(error =>
+            `.${e.target.name}` === error.dataPath);
+          errors = errors.map(error => error.message);
+        }
+        this.errors[e.target.name] = errors;
+      },
+      getErrors: () => this.errors[attrs.name],
+      value: this.data[attrs.name],
+    };
+    // add the given attributes
+    Object.keys(attrs).forEach((key) => { boundFormelement[key] = attrs[key]; });
+
+    return boundFormelement;
+  }
+
+  patchOnClick(patchableFields) {
+    return {
+      onclick: () => {
+        if (this.changed) {
+          getSession().then((apiSession) => {
+            // fields like `_id` are not patchable and would lead to an error
+            // We therefore only send patchable fields
+            const patchData = {};
+            patchableFields.forEach((key) => {
+              patchData[key] = this.data[key];
+            });
+
+            apiSession.patch(`${this.resource}/${this.id}`, patchData, {
+              headers: { 'If-Match': this.data._etag },
+            }).then(() => { this.callback(); });
+          });
+        } else {
+          this.callback();
+        }
+      },
+    };
+  }
+}
+
+export class inputGroup {
+  constructor(vnode) {
+    // Link the error-getting function from the binding
+    this.getErrors = () => [];
+    if (vnode.attrs.getErrors) {
+      this.getErrors = vnode.attrs.getErrors;
+    }
+  }
+
+  view(vnode) {
+    // set display-settings accoridng to error-state
+    let errorField = null;
+    const groupClasses = vnode.attrs.classes ? vnode.attrs.classes : [];
+    const errors = this.getErrors();
+    if (errors.length > 0) {
+      errorField = m('span.help-block', `Error: ${errors.join(', ')}`);
+      groupClasses.push('has-error');
+    }
+
+    return m('div.form-group', { class: groupClasses }, [
+      m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+      m(`input[name=${vnode.attrs.name}][id=${vnode.attrs.name}].form-control`, {
+        value: vnode.attrs.value, onchange: vnode.attrs.onchange,
+      }),
+      errorField,
+    ]);
+  }
+}
+
+export class selectGroup {
+  view(vnode) {
+    return m('div.form-group', { class: vnode.attrs.classes }, [
+      m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+      m(
+        `select[name=${vnode.attrs.name}][id=${vnode.attrs.name}].form-control`,
+        { value: vnode.attrs.value, onchange: vnode.attrs.onchange },
+        vnode.attrs.options.map(option => m('option', option)),
+      ),
+    ]);
+  }
+}
diff --git a/src/views/itemView.js b/src/views/itemView.js
new file mode 100644
index 0000000000000000000000000000000000000000..6cc828033e11db70b3fcc21fb26c3bb98dee7c49
--- /dev/null
+++ b/src/views/itemView.js
@@ -0,0 +1,32 @@
+import { getSession } from '../auth';
+
+const m = require('mithril');
+
+export class ItemView {
+  constructor(resource) {
+    this.data = null;
+    this.id = m.route.param('id');
+    this.resource = resource;
+  }
+
+  loadItemData(session) {
+    session.get(`${this.resource}/${this.id}`).then((response) => {
+      this.data = response.data;
+      m.redraw();
+    });
+  }
+
+  oninit() {
+    getSession().then((apiSession) => {
+      this.loadItemData(apiSession);
+    }).catch(() => {
+      m.route.set('/login');
+    });
+  }
+}
+
+export class Title {
+  view(vnode) {
+    return m('h1', vnode.attrs);
+  }
+}
diff --git a/src/views/tableView.js b/src/views/tableView.js
new file mode 100644
index 0000000000000000000000000000000000000000..b70e86e2231998ce47f96c7be23c4befdf9390f4
--- /dev/null
+++ b/src/views/tableView.js
@@ -0,0 +1,57 @@
+import { getSession } from '../auth';
+
+const m = require('mithril');
+
+class TableRow {
+  view(vnode) {
+    return m(
+      'tr',
+      { onclick() { m.route.set(`/${vnode.attrs.data._links.self.href}`); } },
+      vnode.attrs.show_keys.map((key) => {
+        // Access a nested key, indicated by dot-notation
+        let data = vnode.attrs.data;
+        key.split('.').forEach((subKey) => { data = data[subKey]; });
+        return m('td', data);
+      }),
+    );
+  }
+}
+
+export default class TableView {
+  constructor(vnode) {
+    this.items = [];
+    this.show_keys = vnode.attrs.keys;
+    this.resource = vnode.attrs.resource;
+    // the querystring is either given or will be parsed from the url
+    if (vnode.attrs.querystring) {
+      this.querystring = vnode.attrs.querystring;
+    } else {
+      this.querystring = m.buildQueryString(m.route.param());
+    }
+  }
+
+  oninit() {
+    getSession().then((apiSession) => {
+      let url = this.resource;
+      if (this.querystring.length > 0) url += `?${this.querystring}`;
+      apiSession.get(url).then((response) => {
+        this.items = response.data._items;
+        console.log(this.items);
+        m.redraw();
+      });
+    }).catch(() => {
+      m.route.set('/login');
+    });
+  }
+
+  view() {
+    return m('div', [
+      m('table.table.table-hover', [
+        m('thead', m('tr', this.show_keys.map(title => m('th', title)))),
+        m('tbody', this.items.map(item =>
+          m(TableRow, { show_keys: this.show_keys, data: item }))),
+      ]),
+    ]);
+  }
+}
+