diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index b7a3b21..3a144cc 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -3,8 +3,13 @@
     <Header :user="user" />
 
     <div class="body">
-      <Loading v-if="loading" />
-      <RouterView />
+      <div class="loading-container" v-if="loading">
+        <Loading />
+      </div>
+
+      <div class="view-container" v-else>
+        <RouterView />
+      </div>
     </div>
 
     <Messages />
@@ -130,12 +135,6 @@ export default {
         &.right {
           margin-right: 0.5rem;
         }
-
-        .logout-text {
-          @include mobile {
-            display: none;
-          }
-        }
       }
 
       .spacer {
@@ -151,5 +150,23 @@ export default {
     overflow-y: auto;
     position: relative;
   }
+
+  .loading-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.5);
+    z-index: 1000;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .view-container {
+    width: 100%;
+    height: 100%;
+  }
 }
 </style>
diff --git a/frontend/src/components/Header.vue b/frontend/src/components/Header.vue
index 33fd443..0937729 100644
--- a/frontend/src/components/Header.vue
+++ b/frontend/src/components/Header.vue
@@ -42,8 +42,8 @@
 import { RouterLink } from 'vue-router'
 
 import { type Optional } from '../models/Types';
-import Dropdown from './Dropdown.vue';
-import DropdownItem from './DropdownItem.vue';
+import Dropdown from '../elements/Dropdown.vue';
+import DropdownItem from '../elements/DropdownItem.vue';
 import User from '../models/User';
 
 export default {
@@ -86,6 +86,8 @@ header {
         margin-left: 0.5rem;
 
         :deep(a) {
+          color: var(--color-accent);
+
           &:hover {
             background: none;
             font-size: 1.5rem;
@@ -109,7 +111,7 @@ header {
       }
 
       .logout-text {
-        @include mobile {
+        @include media(mobile) {
           display: none;
         }
       }
diff --git a/frontend/src/components/Map.vue b/frontend/src/components/Map.vue
index 84cfd08..33acddb 100644
--- a/frontend/src/components/Map.vue
+++ b/frontend/src/components/Map.vue
@@ -8,7 +8,7 @@
             <div class="key">From</div>
             <div class="value">
               <a href="#" @click.prevent.stop="onStartDateClick">
-                {{ displayDate(oldestPoint.timestamp) }}
+                {{ formatDate(oldestPoint.timestamp) }}
               </a>
             </div>
           </div>
@@ -16,7 +16,7 @@
             <div class="key">To</div>
             <div class="value">
               <a href="#" @click.prevent.stop="onEndDateClick">
-                {{ displayDate(newestPoint.timestamp) }}
+                {{ formatDate(newestPoint.timestamp) }}
               </a>
             </div>
           </div>
@@ -392,7 +392,7 @@ main {
     padding: 0.5em;
     z-index: 1;
 
-    @include mobile {
+    @include media(mobile) {
       bottom: 1em;
     }
 
@@ -407,8 +407,9 @@ main {
     top: 0;
     right: 0;
     padding: 0.5em;
-    background-color: rgba(255, 255, 255, 0.8);
+    background: var(--color-background);
     border-radius: 0.25em;
+    opacity: 0.8;
     z-index: 1;
 
     .row {
diff --git a/frontend/src/components/PointInfo.vue b/frontend/src/components/PointInfo.vue
index 7b1bb85..4c535d7 100644
--- a/frontend/src/components/PointInfo.vue
+++ b/frontend/src/components/PointInfo.vue
@@ -78,7 +78,7 @@ export default {
     },
 
     timeString(): string | null {
-      return this.displayDate(this.point?.timestamp)
+      return this.formatDate(this.point?.timestamp)
     },
   },
 
diff --git a/frontend/src/components/devices/Device.vue b/frontend/src/components/devices/Device.vue
new file mode 100644
index 0000000..557a039
--- /dev/null
+++ b/frontend/src/components/devices/Device.vue
@@ -0,0 +1,292 @@
+<template>
+  <div class="device" @click="showDetails = !showDetails">
+    <div class="loading-container" v-if="loading">
+      <Loading />
+    </div>
+
+    <div class="header" :class="{ expanded: showDetails }">
+      <h2>
+        {{ device.name }}
+      </h2>
+
+      <div class="buttons wide" @click.stop>
+        <button type="button" title="Edit" @click="showForm = true">
+          <font-awesome-icon icon="edit" />&nbsp;
+        </button>
+
+        <button type="button" title="Delete" @click="showDeleteDialog = true">
+          <font-awesome-icon icon="trash" />&nbsp;
+        </button>
+
+        <button type="button" title="Details" @click="showDetails = !showDetails">
+          <font-awesome-icon icon="info-circle" />&nbsp;
+        </button>
+      </div>
+
+      <div class="buttons mobile" @click.stop>
+        <Dropdown>
+          <template #button>
+            <button class="options" title="Options">
+              <font-awesome-icon icon="ellipsis-h" />
+            </button>
+          </template>
+
+          <DropdownItem @click="showForm = true">
+            <template #icon>
+              <font-awesome-icon icon="edit" />
+            </template>
+            Edit
+          </DropdownItem>
+
+          <DropdownItem @click="showDeleteDialog = true">
+            <template #icon>
+              <font-awesome-icon icon="trash" />
+            </template>
+            Delete
+          </DropdownItem>
+
+          <DropdownItem @click="showDetails = !showDetails">
+            <template #icon>
+              <font-awesome-icon icon="info-circle" />
+            </template>
+            Details
+          </DropdownItem>
+        </Dropdown>
+      </div>
+    </div>
+
+    <div class="details" v-if="showDetails" @click.stop>
+      <div class="row">
+        <div class="property">
+          Device ID
+        </div>
+        <div class="value">
+          {{ device.id }}
+        </div>
+      </div>
+
+      <div class="row creation-date">
+        <div class="property">
+          Creation Date
+        </div>
+        <div class="value">
+          {{ formatDate(device.createdAt) }}
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <Modal :visible="true" @close="clearForm" v-if="showForm">
+    <template v-slot:title>
+      Update device
+    </template>
+
+    <DeviceForm
+        :device="device"
+        @close="clearForm"
+        @input="onUpdate" />
+  </Modal>
+
+  <ConfirmDialog
+      :visible="showDeleteDialog"
+      :disabled="loading"
+      v-if="showDeleteDialog"
+      @close="showDeleteDialog = false"
+      @confirm="runDelete">
+    <template #title>
+      Delete device
+    </template>
+
+    <p>
+      Are you sure you want to delete the device <b>{{ device.name }}</b>?<br />
+      This action cannot be undone and all the data associated with this device will be lost.
+    </p>
+  </ConfirmDialog>
+</template>
+
+<script lang="ts">
+import Api from '../../mixins/Api.vue';
+import ConfirmDialog from '../../elements/ConfirmDialog.vue';
+import Dates from '../../mixins/Dates.vue';
+import DeviceForm from '../../components/devices/DeviceForm.vue';
+import Dropdown from '../../elements/Dropdown.vue';
+import DropdownItem from '../../elements/DropdownItem.vue';
+import Loading from '../../elements/Loading.vue';
+import Modal from '../../elements/Modal.vue';
+import UserDevice from '../../models/UserDevice';
+
+export default {
+  emits: ['delete', 'update'],
+  mixins: [
+    Api,
+    Dates,
+  ],
+
+  components: {
+    ConfirmDialog,
+    DeviceForm,
+    Dropdown,
+    DropdownItem,
+    Loading,
+    Modal,
+  },
+
+  props: {
+    device: {
+      type: Object as () => UserDevice,
+      required: true,
+    },
+  },
+
+  data() {
+    return {
+      loading: false,
+      showDeleteDialog: false,
+      showDetails: false,
+      showForm: false,
+    }
+  },
+
+  methods: {
+    async runDelete() {
+      this.showDeleteDialog = false;
+      this.loading = true;
+
+      try {
+        await this.deleteDevice(this.device.id);
+        this.$emit('delete', this.device);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    clearForm() {
+      this.showForm = false;
+    },
+
+    onUpdate(device: UserDevice) {
+      this.clearForm();
+      this.$emit('update', device);
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@use "@/styles/common.scss" as *;
+
+.device {
+  width: 100%;
+  margin: 0;
+  padding: 0 0.5rem;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  font-size: 1rem;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+  cursor: pointer;
+
+  @include media(tablet) {
+    min-width: 30em;
+  }
+
+  .header {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    padding: 0.5em 0;
+
+    &:hover {
+      h2 {
+        color: var(--color-hover);
+      }
+    }
+
+    &.expanded {
+      background-color: rgba(0, 0, 0, 0.05);
+      font-weight: bold;
+      padding: 0.5em;
+      border-radius: 0.75rem;
+    }
+  }
+
+  h2 {
+    font-size: 1.25em;
+    padding: 0.5em 0;
+    flex: 1;
+  }
+
+  .buttons {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    display: none;
+
+    &.mobile {
+      display: flex;
+    }
+
+    &.wide {
+      display: none;
+    }
+
+    @include media(tablet) {
+      &.mobile {
+        display: none !important;
+      }
+
+      &.wide {
+        display: flex !important;
+      }
+    }
+
+    :deep(button) {
+      background-color: transparent;
+      color: var(--color-text);
+      border: none;
+      cursor: pointer;
+      font-size: 1em;
+      opacity: 0.75;
+
+      &:hover {
+        opacity: 1;
+      }
+    }
+  }
+
+  .details {
+    cursor: initial;
+    animation: fade-in 0.5s;
+    padding: 0.5em 0;
+
+    .row {
+      display: flex;
+      flex-direction: column;
+
+      @include media(tablet) {
+        flex-direction: row;
+        justify-content: space-between;
+      }
+
+      &:hover {
+        .property {
+          color: var(--color-hover);
+        }
+      }
+
+      .property {
+        font-weight: bold;
+        margin-bottom: 0.25em;
+
+        @include media(tablet) {
+          margin-bottom: 0;
+        }
+      }
+
+      .value {
+        font-size: 0.9em;
+      }
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/devices/DeviceForm.vue b/frontend/src/components/devices/DeviceForm.vue
new file mode 100644
index 0000000..b771f0f
--- /dev/null
+++ b/frontend/src/components/devices/DeviceForm.vue
@@ -0,0 +1,118 @@
+<template>
+  <form @submit.prevent="sync" @input.stop.prevent>
+    <div class="loading-container" v-if="loading">
+      <Loading />
+    </div>
+
+    <div class="row">
+      <label for="name">
+        Enter a name for your device.
+      </label>
+
+      <input type="text"
+             ref="name"
+             placeholder="Device name"
+             :value="device?.name" />
+    </div>
+
+    <div class="row buttons">
+      <button type="submit">
+        {{ device ? 'Update' : 'Register' }}
+      </button>
+
+      <button type="button" @click="$emit('close')">
+        Cancel
+      </button>
+    </div>
+  </form>
+</template>
+
+<script lang="ts">
+import { Optional } from '../../models/Types';
+import Api from '../../mixins/Api.vue';
+import Loading from '../../elements/Loading.vue';
+import Notifications from '../../mixins/Notifications.vue';
+import UserDevice from '../../models/UserDevice';
+
+export default {
+  emits: ['close', 'input'],
+  mixins: [
+    Api,
+    Notifications,
+  ],
+
+  components: {
+    Loading,
+  },
+
+  props: {
+    device: {
+      type: Object as () => Optional<UserDevice>,
+      default: null,
+    },
+  },
+
+  data() {
+    return {
+      devices: [] as UserDevice[],
+      loading: false,
+    }
+  },
+
+  methods: {
+    async sync() {
+      const name = this.$refs.name.value.trim();
+      if (!name?.length) {
+        this.notify({
+          content: 'Please enter a name for your device.',
+          isError: true,
+        });
+
+        return;
+      }
+
+      this.loading = true;
+      try {
+        const device = this.device
+            ? await this.updateDevice({
+              ...this.device,
+              name,
+            })
+            : await this.registerDevice(name);
+
+        if (!device) {
+          return;
+        }
+
+        this.$emit('input', device);
+      } finally {
+        this.loading = false;
+      }
+    },
+  },
+
+  async mounted() {
+    this.$refs.name.focus();
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+form {
+  width: 100%;
+  max-width: 300px;
+  margin: 0 auto;
+  position: relative;
+
+  .buttons {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    margin-top: 1em;
+
+    button {
+      height: 2.3em;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/devices/DevicesList.vue b/frontend/src/components/devices/DevicesList.vue
new file mode 100644
index 0000000..89a646e
--- /dev/null
+++ b/frontend/src/components/devices/DevicesList.vue
@@ -0,0 +1,66 @@
+<template>
+  <div class="devices-list">
+    <h1>
+      <font-awesome-icon icon="mobile-alt" />&nbsp;
+      My devices
+    </h1>
+
+    <ul>
+      <li v-for="device in devices" :key="device.id">
+        <Device :device="device"
+                @delete="$emit('delete', device)"
+                @update="$emit('update', $event)" />
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts">
+import Device from './Device.vue';
+import UserDevice from '../../models/UserDevice';
+
+export default {
+  emits: ['delete', 'update'],
+  components: {
+    Device,
+  },
+
+  props: {
+    devices: {
+      type: Array as () => UserDevice[],
+      required: true,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/common.scss' as *;
+
+.devices-list {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+
+  @include media(tablet) {
+    min-width: 30em;
+  }
+
+  h1 {
+    width: 100%;
+    text-align: center;
+    padding: 0.5em 0;
+  }
+
+  ul {
+    list-style-type: none;
+    padding: 0;
+    margin: 0;
+    width: 100%;
+    overflow-y: auto;
+  }
+}
+</style>
diff --git a/frontend/src/components/filter/Form.vue b/frontend/src/components/filter/Form.vue
index 052b2ed..8a8a850 100644
--- a/frontend/src/components/filter/Form.vue
+++ b/frontend/src/components/filter/Form.vue
@@ -326,7 +326,7 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-@use "@/styles/common.scss";
+@use "@/styles/common.scss" as *;
 
 .filter-view {
   height: 100%;
@@ -341,27 +341,28 @@ export default {
   margin-bottom: 0.25em;
   box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.66);
 
-  @include common.tablet {
-    min-width: 45em;
+  @include media(mobile) {
+    width: calc(100vw - 2em);
   }
 
-  @include common.desktop {
+  @include media(tablet) {
+    width: 100%;
     min-width: 45em;
   }
 
-  @include common.mobile {
-    width: 100vw;
-  }
-
   .date-selectors {
     display: flex;
     justify-content: space-between;
     width: 100%;
 
-    @include common.mobile {
+    @include media(mobile) {
       flex-direction: column;
     }
 
+    @include media(tablet) {
+      flex-direction: row;
+    }
+
     .date-selector {
       display: flex;
       flex-direction: column;
diff --git a/frontend/src/elements/ConfirmDialog.vue b/frontend/src/elements/ConfirmDialog.vue
new file mode 100644
index 0000000..dbb88f9
--- /dev/null
+++ b/frontend/src/elements/ConfirmDialog.vue
@@ -0,0 +1,43 @@
+<template>
+  <Modal :visible="visible" @close="$emit('close')">
+    <template #title>
+      <slot name="title" />
+    </template>
+
+    <form class="confirm-dialog" @submit.prevent="$emit('confirm')">
+      <div class="wrapper">
+        <div class="content">
+          <slot />
+        </div>
+        <div class="buttons">
+          <button type="submit" @click="$emit('confirm')" :disabled="disabled">Confirm</button>
+          <button type="button" @click="$emit('close')" :disabled="disabled">Cancel</button>
+        </div>
+      </div>
+    </form>
+  </Modal>
+</template>
+
+<script lang="ts">
+import Modal from './Modal.vue';
+
+export default {
+  emits: ['close', 'confirm'],
+  components: {
+    Modal,
+  },
+  mixins: [
+    Modal,
+  ],
+
+  props: {
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/frontend/src/components/Dropdown.vue b/frontend/src/elements/Dropdown.vue
similarity index 90%
rename from frontend/src/components/Dropdown.vue
rename to frontend/src/elements/Dropdown.vue
index ce0c100..856f913 100644
--- a/frontend/src/components/Dropdown.vue
+++ b/frontend/src/elements/Dropdown.vue
@@ -4,7 +4,7 @@
       <slot name="button" />
     </button>
 
-    <div class="dropdown__container" ref="container">
+    <div class="dropdown__container" ref="container" @click="hide">
       <div class="dropdown__content">
         <slot />
       </div>
@@ -21,6 +21,10 @@ export default {
   },
 
   methods: {
+    hide() {
+      this.container.classList.remove('show');
+    },
+
     show() {
       this.container.classList.add('show');
     },
@@ -39,6 +43,7 @@ export default {
     background: none;
     border: none;
     cursor: pointer;
+    margin: 0;
 
     &:focus,
     &:hover {
diff --git a/frontend/src/components/DropdownItem.vue b/frontend/src/elements/DropdownItem.vue
similarity index 96%
rename from frontend/src/components/DropdownItem.vue
rename to frontend/src/elements/DropdownItem.vue
index 0a68eb8..9d881ea 100644
--- a/frontend/src/components/DropdownItem.vue
+++ b/frontend/src/elements/DropdownItem.vue
@@ -32,7 +32,7 @@ export default {
   }
 
   &__icon {
-    margin-right: 0.5rem;
+    margin: 0 0.5rem;
   }
 
   &__text {
diff --git a/frontend/src/elements/FloatingButton.vue b/frontend/src/elements/FloatingButton.vue
new file mode 100644
index 0000000..e85491d
--- /dev/null
+++ b/frontend/src/elements/FloatingButton.vue
@@ -0,0 +1,51 @@
+<template>
+  <button class="floating-button"
+          @click="$emit('click')"
+          :title="title"
+          :aria-label="title">
+    <font-awesome-icon :icon="icon" />
+  </button>
+</template>
+
+<script lang="ts">
+export default {
+  emits: ['click'],
+  props: {
+    icon: {
+      type: String,
+      required: true,
+    },
+
+    title: {
+      type: String,
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+button.floating-button {
+  position: fixed;
+  bottom: 1em;
+  right: 1em;
+  background: var(--color-accent);
+  color: var(--color-background);
+  width: 4em;
+  height: 4em;
+  font-size: 1em;
+  padding: 1.5em;
+  outline: none;
+  border: none;
+  border-radius: 50%;
+  box-shadow: 1px 1px 2px 2px var(--color-border);
+  cursor: pointer;
+  z-index: 100;
+
+  &:hover {
+    background: var(--color-accent) !important;
+    color: var(--color-background) !important;
+    font-weight: bold;
+    filter: brightness(1.2);
+  }
+}
+</style>
diff --git a/frontend/src/elements/Modal.vue b/frontend/src/elements/Modal.vue
new file mode 100644
index 0000000..05667d7
--- /dev/null
+++ b/frontend/src/elements/Modal.vue
@@ -0,0 +1,88 @@
+<template>
+  <div class="modal-container" @click="close" v-if="visible">
+    <div class="modal" @click.stop>
+      <div class="modal-header">
+        <h1>
+          <slot name="title" />
+        </h1>
+
+        <button class="close" title="Close" @click="close">
+          <font-awesome-icon icon="times" />
+        </button>
+      </div>
+
+      <div class="modal-body">
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  emits: ['close'],
+  props: {
+    visible: {
+      type: Boolean,
+    },
+  },
+
+  methods: {
+    close() {
+      this.$emit('close');
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.modal-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: rgba(0, 0, 0, 0.5);
+  z-index: 1000;
+
+  .modal {
+    min-width: 30em;
+    background-color: var(--color-background);
+    border-radius: 0.5em;
+    box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
+    overflow: hidden;
+    animation: fade-in 0.5s;
+
+    .modal-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 0.25em 1em;
+      background-color: rgba(0, 0, 0, 0.05);
+      border-bottom: 1px solid var(--color-border);
+
+      h1 {
+        font-size: 1.25em;
+        margin: 0;
+      }
+
+      .close {
+        background: none;
+        margin: 0;
+        padding: 0.25em 0;
+        cursor: pointer;
+        font-size: 1.5em;
+        border: none;
+      }
+    }
+
+    .modal-body {
+      padding: 1em;
+      overflow-y: auto;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/mixins/Api.vue b/frontend/src/mixins/Api.vue
index f20615e..84a0654 100644
--- a/frontend/src/mixins/Api.vue
+++ b/frontend/src/mixins/Api.vue
@@ -1,11 +1,13 @@
 <script lang="ts">
 import Auth from './api/Auth.vue'
+import Devices from './api/Devices.vue'
 import GPSData from './api/GPSData.vue'
 import Users from './api/Users.vue'
 
 export default {
   mixins: [
     Auth,
+    Devices,
     GPSData,
     Users,
   ],
diff --git a/frontend/src/mixins/Dates.vue b/frontend/src/mixins/Dates.vue
index 089b3e9..60bae27 100644
--- a/frontend/src/mixins/Dates.vue
+++ b/frontend/src/mixins/Dates.vue
@@ -1,7 +1,7 @@
 <script lang="ts">
 export default {
   methods: {
-    displayDate(date: Date | number | string | null | undefined): string {
+    formatDate(date: Date | number | string | null | undefined): string {
       if (!date) {
         return '-'
       }
diff --git a/frontend/src/mixins/Notifications.vue b/frontend/src/mixins/Notifications.vue
new file mode 100644
index 0000000..2060db9
--- /dev/null
+++ b/frontend/src/mixins/Notifications.vue
@@ -0,0 +1,9 @@
+<script lang="ts">
+export default {
+  methods: {
+    notify(payload: any) {
+      this.$msgBus.emit('message', payload);
+    },
+  },
+}
+</script>
diff --git a/frontend/src/mixins/api/Devices.vue b/frontend/src/mixins/api/Devices.vue
new file mode 100644
index 0000000..f138b4d
--- /dev/null
+++ b/frontend/src/mixins/api/Devices.vue
@@ -0,0 +1,41 @@
+<script lang="ts">
+import UserDevice from '../../models/UserDevice';
+import Common from './Common.vue';
+
+export default {
+  mixins: [Common],
+  methods: {
+    async getMyDevices(): Promise<UserDevice[]> {
+      return (
+        await this.request(`/devices`) as {
+          devices: UserDevice[]
+        }
+      ).devices;
+    },
+
+    async registerDevice(name: string): Promise<UserDevice> {
+      return await this.request(`/devices`, {
+        method: 'POST',
+        body: { name },
+      }) as UserDevice;
+    },
+
+    async updateDevice(device: UserDevice): Promise<UserDevice> {
+      return await this.request(`/devices/${device.id}`, {
+        method: 'PATCH',
+        body: Object.keys(device).reduce((acc, key) => {
+          if (!['id', 'userId', 'createdAt', 'updatedAt'].includes(key)) {
+            acc[key] = device[key];
+          }
+
+          return acc;
+        }, {} as Record<string, unknown>),
+      }) as UserDevice;
+    },
+
+    async deleteDevice(id: string): Promise<void> {
+      await this.request(`/devices/${id}`, { method: 'DELETE' });
+    },
+  },
+}
+</script>
diff --git a/frontend/src/models/UserDevice.ts b/frontend/src/models/UserDevice.ts
new file mode 100644
index 0000000..eb9376d
--- /dev/null
+++ b/frontend/src/models/UserDevice.ts
@@ -0,0 +1,27 @@
+import type { Optional } from "./Types";
+
+class UserDevice {
+  public id: string;
+  public userId: number;
+  public name: string;
+  public createdAt?: Optional<Date>;
+
+  constructor({
+    id,
+    userId,
+    name,
+    createdAt = null,
+  }: {
+   id: string,
+   userId: number,
+   name: string,
+   createdAt?: Optional<Date>,
+  }) {
+    this.id = id;
+    this.userId = userId;
+    this.name = name;
+    this.createdAt = createdAt;
+  }
+}
+
+export default UserDevice;
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index fc50124..bae97bd 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -1,4 +1,5 @@
 import { createRouter, createWebHistory } from 'vue-router'
+import Devices from '../views/Devices.vue'
 import HomeView from '../views/HomeView.vue'
 import Login from '../views/Login.vue'
 import Logout from '../views/Logout.vue'
@@ -12,6 +13,12 @@ const router = createRouter({
       component: HomeView,
     },
 
+    {
+      path: '/devices',
+      name: 'devices',
+      component: Devices,
+    },
+
     {
       path: '/login',
       name: 'login',
@@ -23,15 +30,6 @@ const router = createRouter({
       name: 'logout',
       component: Logout,
     },
-
-    //{
-    //  path: '/about',
-    //  name: 'about',
-    //  // route level code-splitting
-    //  // this generates a separate chunk (About.[hash].js) for this route
-    //  // which is lazy-loaded when the route is visited.
-    //  component: () => import('../views/AboutView.vue'),
-    //},
   ],
 })
 
diff --git a/frontend/src/views/Devices.vue b/frontend/src/views/Devices.vue
new file mode 100644
index 0000000..07dd48b
--- /dev/null
+++ b/frontend/src/views/Devices.vue
@@ -0,0 +1,128 @@
+<template>
+  <div class="devices">
+    <div class="wrapper">
+      <div class="loading-container" v-if="loading">
+        <Loading />
+      </div>
+
+      <DevicesList :devices="devices"
+                   @delete="onDelete"
+                   @update="onUpdate" />
+    </div>
+
+    <Modal :visible="showDeviceForm" @close="clearForm">
+      <template v-slot:title>
+        Register a new device
+      </template>
+
+      <DeviceForm
+          @close="clearForm"
+          @input="addDevice" />
+    </Modal>
+
+    <FloatingButton
+        icon="fas fa-plus"
+        title="Register a new device"
+        @click="showDeviceForm = true" />
+  </div>
+</template>
+
+<script lang="ts">
+import DeviceForm from '../components/devices/DeviceForm.vue';
+import Api from '../mixins/Api.vue';
+import DevicesList from '../components/devices/DevicesList.vue';
+import FloatingButton from '../elements/FloatingButton.vue';
+import Loading from '../elements/Loading.vue';
+import Modal from '../elements/Modal.vue';
+import UserDevice from '../models/UserDevice';
+
+export default {
+  mixins: [Api],
+  components: {
+    DeviceForm,
+    DevicesList,
+    FloatingButton,
+    Loading,
+    Modal,
+  },
+
+  data() {
+    return {
+      devices: [] as UserDevice[],
+      loading: false,
+      showDeviceForm: false,
+    }
+  },
+
+  computed: {
+    deviceIndexById() {
+      return this.devices.reduce(
+        (acc: Record<string, number>, device: UserDevice, index: number) => {
+          acc[device.id] = index;
+          return acc;
+        }, {} as Record<string, number>
+      );
+    },
+  },
+
+  methods: {
+    addDevice(device: UserDevice) {
+      this.devices.push(device);
+      this.clearForm();
+    },
+
+    clearForm() {
+      this.showDeviceForm = false;
+    },
+
+    onDelete(device: UserDevice) {
+      const index = this.deviceIndexById[device.id];
+      this.devices.splice(index, 1);
+    },
+
+    onUpdate(device: UserDevice) {
+      const index = this.deviceIndexById[device.id];
+      this.devices[index] = device;
+    },
+  },
+
+  async mounted() {
+    this.loading = true;
+    try {
+      this.devices = await this.getMyDevices();
+    } finally {
+      this.loading = false;
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/common.scss' as *;
+
+.devices {
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.05);
+  display: flex;
+  justify-content: center;
+  padding: 2em;
+
+  .wrapper {
+    height: 100%;
+    display: flex;
+    background-color: var(--color-background);
+    border-radius: 1em;
+    box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.2);
+    position: relative;
+
+    @include media(mobile) {
+      width: 100%;
+    }
+
+    @include media(tablet) {
+      min-width: 30em;
+      max-width: 40em;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue
index 15b7115..9a1d469 100644
--- a/frontend/src/views/Login.vue
+++ b/frontend/src/views/Login.vue
@@ -105,7 +105,7 @@ main {
   justify-content: center;
   height: 100%;
   width: 100%;
-  background: var(--vt-c-green-bg-light);
+  background: var(--color-accent);
 }
 
 form {