diff --git a/components/ContactBasicData.vue b/components/ContactBasicData.vue
new file mode 100644
index 0000000000000000000000000000000000000000..13011c1d228497670cdac2afba12feb052fa0026
--- /dev/null
+++ b/components/ContactBasicData.vue
@@ -0,0 +1,93 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020-2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <div>
+    <v-row>
+      <v-col cols="12" md="3">
+        <label>Given name</label>
+        {{ value.givenName | orDefault }}
+      </v-col>
+      <v-col cols="12" md="3">
+        <label>Family name</label>
+        {{ value.familyName | orDefault }}
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col cols="12" md="9">
+        <label>E-mail</label>
+        {{ value.email | orDefault }}
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col cols="12" md="9">
+        <label>Website</label>
+        {{ value.website | orDefault }}
+        <a v-if="value.website.length > 0" :href="value.website" target="_blank">
+          <v-icon>
+            mdi-open-in-new
+          </v-icon>
+        </a>
+      </v-col>
+    </v-row>
+  </div>
+</template>
+<style lang="scss">
+@import '~vuetify/src/styles/settings/variables';
+@import '~vuetify/src/styles/settings/colors';
+
+label {
+  /* TODO: move to its own file */
+  display: block;
+  font-size: map-deep-get($headings, 'caption', 'size');
+  font-weight: map-deep-get($headings, 'caption', 'weight');
+  letter-spacing: map-deep-get($headings, 'caption', 'letter-spacing');
+  line-height: map-deep-get($headings, 'caption', 'line-height');
+  font-family: map-deep-get($headings, 'caption', 'font-family');
+  color: map-get($grey, 'darken-1');
+}
+</style>
+
+<script lang="ts">
+import { Component, Vue, Prop } from 'nuxt-property-decorator'
+
+import { Contact } from '@/models/Contact'
+
+@Component
+export default class ContactBasicData extends Vue {
+  @Prop({
+    default: () => new Contact(),
+    required: true,
+    type: Contact
+  })
+  readonly value!: Contact
+}
+
+</script>
diff --git a/components/ContactBasicDataForm.vue b/components/ContactBasicDataForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..82bbfc18178f310615965576241fe6949a33503b
--- /dev/null
+++ b/components/ContactBasicDataForm.vue
@@ -0,0 +1,156 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020-2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <v-form
+    ref="basicForm"
+    @submit.prevent
+  >
+    <v-row>
+      <v-col cols="12" md="3">
+        <v-text-field
+          :value="value.givenName"
+          label="Given name"
+          :readonly="readonly"
+          :disabled="readonly"
+          required
+          class="required"
+          :rules="[rules.required]"
+          @input="update('givenName', $event)"
+        />
+      </v-col>
+      <v-col cols="12" md="3">
+        <v-text-field
+          :value="value.familyName"
+          label="Family name"
+          :readonly="readonly"
+          :disabled="readonly"
+          required
+          class="required"
+          :rules="[rules.required]"
+          @input="update('familyName', $event)"
+        />
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col cols="12" md="9">
+        <v-text-field
+          :value="value.email"
+          label="E-mail"
+          type="email"
+          :readonly="readonly"
+          :disabled="readonly"
+          required
+          class="required"
+          :rules="[rules.required]"
+          @input="update('email', $event)"
+        />
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col cols="12" md="9">
+        <v-text-field
+          v-if="readonly"
+          :value="value.website"
+          label="Website"
+          placeholder="https://"
+          type="url"
+          :readonly="true"
+          :disabled="true"
+        >
+          <template slot="append">
+            <a v-if="value.website.length > 0" :href="value.website" target="_blank">
+              <v-icon>
+                mdi-open-in-new
+              </v-icon>
+            </a>
+          </template>
+        </v-text-field>
+        <v-text-field
+          v-else
+          :value="value.website"
+          label="Website"
+          placeholder="https://"
+          type="url"
+          @input="update('email', $event)"
+        />
+      </v-col>
+    </v-row>
+  </v-form>
+</template>
+<script lang="ts">
+import { Component, Prop, mixins } from 'nuxt-property-decorator'
+
+import { Rules } from '@/mixins/Rules'
+
+import { Contact } from '@/models/Contact'
+
+@Component
+export default class ContactBasicDataForm extends mixins(Rules) {
+  @Prop({
+    required: true,
+    type: Contact
+  })
+  readonly value!: Contact
+
+  @Prop({
+    default: () => false,
+    type: Boolean
+  })
+  readonly readonly!: boolean
+
+  update (key: string, value: string) {
+    const newObj = Contact.createFromObject(this.value)
+    switch (key) {
+      case 'givenName':
+        newObj.givenName = value
+        break
+      case 'familyName':
+        newObj.familyName = value
+        break
+      case 'email':
+        newObj.email = value
+        break
+      case 'website':
+        newObj.website = value
+        break
+      default:
+        throw new TypeError('key ' + key + ' is not valid')
+    }
+
+    this.$emit('input', newObj)
+  }
+
+  public validateForm (): boolean {
+    return (this.$refs.basicForm as Vue & { validate: () => boolean }).validate()
+  }
+}
+
+</script>
diff --git a/pages/contacts/_contactId.vue b/pages/contacts/_contactId.vue
new file mode 100644
index 0000000000000000000000000000000000000000..22aec5567fc53ff1b43b32de66b00b7bc933ffc3
--- /dev/null
+++ b/pages/contacts/_contactId.vue
@@ -0,0 +1,137 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <div>
+    <ProgressIndicator
+      v-model="isLoading"
+    />
+    <v-card flat>
+      <div v-if="isEditPage">
+        <NuxtChild
+          v-model="contact"
+        />
+      </div>
+      <div v-else>
+        <v-card-actions>
+          <v-spacer />
+          <v-btn
+            v-if="isLoggedIn"
+            color="primary"
+            small
+            nuxt
+            :to="'/contacts/' + contactId + '/edit'"
+          >
+            Edit
+          </v-btn>
+        </v-card-actions>
+        <ContactBasicData
+          v-model="contact"
+        />
+        <v-card-actions>
+          <v-spacer />
+          <v-btn
+            v-if="isLoggedIn"
+            color="primary"
+            small
+            nuxt
+            :to="'/contacts/' + contactId + '/edit'"
+          >
+            Edit
+          </v-btn>
+        </v-card-actions>
+      </div>
+    </v-card>
+  </div>
+</template>
+<script lang="ts">
+import { Component, Vue, Watch } from 'nuxt-property-decorator'
+
+import { Contact } from '@/models/Contact'
+
+import ContactBasicData from '@/components/ContactBasicData.vue'
+import ProgressIndicator from '@/components/ProgressIndicator.vue'
+
+@Component({
+  components: {
+    ContactBasicData,
+    ProgressIndicator
+  }
+})
+export default class ContactShowPage extends Vue {
+  private contact: Contact = new Contact()
+  private isLoading: boolean = true
+
+  created () {
+    this.initializeAppBar()
+  }
+
+  mounted () {
+    this.$api.contacts.findById(this.contactId).then((contact) => {
+      this.contact = contact
+      this.isLoading = false
+    }).catch((_error) => {
+      this.$store.commit('snackbar/setError', 'Loading contact failed')
+      this.isLoading = false
+    })
+  }
+
+  beforeDestroy () {
+    this.$store.dispatch('appbar/setDefaults')
+  }
+
+  initializeAppBar () {
+    this.$store.dispatch('appbar/init', {
+      title: 'Show Contact'
+    })
+  }
+
+  get contactId () {
+    return this.$route.params.contactId
+  }
+
+  @Watch('contact', { immediate: true, deep: true })
+  // @ts-ignore
+  onContactChanged (val: Contact) {
+    const fallbackText = this.isEditPage ? 'Edit contact' : 'Show contact'
+    if (val.id) {
+      this.$store.commit('appbar/setTitle', val?.toString() || fallbackText)
+    }
+  }
+
+  get isEditPage () {
+    return this.$route.path === '/contacts/' + this.contactId + '/edit' || this.$route.path === '/contact/' + this.contactId + '/edit/'
+  }
+
+  get isLoggedIn () {
+    return this.$store.getters['oidc/isAuthenticated']
+  }
+}
+</script>
diff --git a/pages/contacts/_contactId/edit.vue b/pages/contacts/_contactId/edit.vue
new file mode 100644
index 0000000000000000000000000000000000000000..48007d15a29495a9132c7cbbc726dcba0d6e700a
--- /dev/null
+++ b/pages/contacts/_contactId/edit.vue
@@ -0,0 +1,154 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <div>
+    <ProgressIndicator
+      v-model="isLoading"
+    />
+    <v-card flat>
+      <v-card-actions>
+        <v-spacer />
+        <v-btn
+          v-if="isLoggedIn"
+          small
+          nuxt
+          :to="'/contacts/' + contactId"
+        >
+          cancel
+        </v-btn>
+        <v-btn
+          v-if="isLoggedIn"
+          color="green"
+          small
+          @click="onSaveButtonClicked"
+        >
+          apply
+        </v-btn>
+      </v-card-actions>
+      <ContactBasicDataForm
+        ref="basicForm"
+        v-model="contactCopy"
+        :readonly="false"
+      />
+      <v-card-actions>
+        <v-spacer />
+        <v-btn
+          v-if="isLoggedIn"
+          small
+          nuxt
+          :to="'/contacts/' + contactId"
+        >
+          cancel
+        </v-btn>
+        <v-btn
+          v-if="isLoggedIn"
+          color="green"
+          small
+          @click="onSaveButtonClicked"
+        >
+          apply
+        </v-btn>
+      </v-card-actions>
+    </v-card>
+  </div>
+</template>
+<script lang="ts">
+import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator'
+
+import { Contact } from '@/models/Contact'
+
+import ContactBasicDataForm from '@/components/ContactBasicDataForm.vue'
+import ProgressIndicator from '@/components/ProgressIndicator.vue'
+
+@Component({
+  components: {
+    ContactBasicDataForm,
+    ProgressIndicator
+  }
+})
+export default class ContactEditPage extends Vue {
+  private contactCopy: Contact = new Contact()
+  private isLoading: boolean = false
+
+  @Prop({
+    required: true,
+    type: Contact
+  })
+  readonly value!: Contact
+
+  mounted () {
+    this.contactCopy = Contact.createFromObject(this.value)
+  }
+
+  onSaveButtonClicked () {
+    if (!(this.$refs.basicForm as Vue & { validateForm: () => boolean }).validateForm()) {
+      this.$store.commit('snackbar/setError', 'Please correct your input')
+      return
+    }
+    this.isLoading = true
+    this.save().then((contact) => {
+      this.isLoading = false
+      this.$emit('input', contact)
+      this.$router.push('/contact/' + this.contactId)
+    }).catch((_error) => {
+      this.isLoading = false
+      this.$store.commit('snackbar/setError', 'Save failed')
+    })
+  }
+
+  save (): Promise<Contact> {
+    return new Promise((resolve, reject) => {
+      this.$api.contacts.save(this.contactCopy).then((savedContact) => {
+        resolve(savedContact)
+      }).catch((_error) => {
+        reject(_error)
+      })
+    })
+  }
+
+  get contactId () {
+    return this.$route.params.contactId
+  }
+
+  @Watch('value', { immediate: true, deep: true })
+  // @ts-ignore
+  onContactChanged (val: Contact) {
+    if (val.id) {
+      this.$store.commit('appbar/setTitle', val?.toString() || 'Edit contact')
+    }
+    this.contactCopy = Contact.createFromObject(val)
+  }
+
+  get isLoggedIn () {
+    return this.$store.getters['oidc/isAuthenticated']
+  }
+}
+</script>
diff --git a/pages/contacts/_id.vue b/pages/contacts/_id.vue
deleted file mode 100644
index 39dc50672dbc0d568e824a1b32fd9ac9dd341209..0000000000000000000000000000000000000000
--- a/pages/contacts/_id.vue
+++ /dev/null
@@ -1,285 +0,0 @@
-<!--
-Web client of the Sensor Management System software developed within the
-Helmholtz DataHub Initiative by GFZ and UFZ.
-
-Copyright (C) 2020
-- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
-- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
-- Helmholtz Centre Potsdam - GFZ German Research Centre for
-  Geosciences (GFZ, https://www.gfz-potsdam.de)
-
-Parts of this program were developed within the context of the
-following publicly funded projects or measures:
-- Helmholtz Earth and Environment DataHub
-  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
-
-Licensed under the HEESIL, Version 1.0 or - as soon they will be
-approved by the "Community" - subsequent versions of the HEESIL
-(the "Licence").
-
-You may not use this work except in compliance with the Licence.
-
-You may obtain a copy of the Licence at:
-https://gitext.gfz-potsdam.de/software/heesil
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the Licence is distributed on an "AS IS" basis,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-implied. See the Licence for the specific language governing
-permissions and limitations under the Licence.
--->
-<template>
-  <div>
-    <v-card outlined>
-      <v-form ref="basicForm">
-        <v-card flat>
-          <v-card-title>
-            {{ title }}
-          </v-card-title>
-          <v-card-text>
-            <v-row>
-              <v-col cols="12" md="3">
-                <v-text-field
-                  v-model="contact.givenName"
-                  label="Given name"
-                  :readonly="readonly"
-                  :disabled="readonly"
-                />
-              </v-col>
-              <v-col cols="12" md="3">
-                <v-text-field
-                  v-model="contact.familyName"
-                  label="Family name"
-                  :readonly="readonly"
-                  :disabled="readonly"
-                />
-              </v-col>
-            </v-row>
-            <v-row>
-              <v-col cols="12" md="9">
-                <v-text-field
-                  v-model="contact.email"
-                  label="E-mail"
-                  type="email"
-                  :readonly="readonly"
-                  :disabled="readonly"
-                />
-              </v-col>
-            </v-row>
-            <v-row>
-              <v-col cols="12" md="9">
-                <v-text-field
-                  v-if="readonly"
-                  v-model="contact.website"
-                  label="Website"
-                  placeholder="https://"
-                  type="url"
-                  :readonly="true"
-                  :disabled="true"
-                >
-                  <template slot="append">
-                    <a v-if="contact.website.length > 0" :href="contact.website" target="_blank">
-                      <v-icon>
-                        mdi-open-in-new
-                      </v-icon>
-                    </a>
-                  </template>
-                </v-text-field>
-                <v-text-field
-                  v-else
-                  v-model="contact.website"
-                  label="Website"
-                  placeholder="https://"
-                  type="url"
-                />
-              </v-col>
-            </v-row>
-          </v-card-text>
-        </v-card>
-      </v-form>
-      <v-btn
-        v-if="!editMode && isLoggedIn"
-        fab
-        fixed
-        bottom
-        right
-        color="secondary"
-        @click="onEditButtonClick"
-      >
-        <v-icon>
-          mdi-pencil
-        </v-icon>
-      </v-btn>
-    </v-card>
-  </div>
-</template>
-
-<style lang="scss">
-@import "@/assets/styles/_forms.scss";
-</style>
-
-<script lang="ts">
-import { Component, Watch, Vue } from 'nuxt-property-decorator'
-
-import { Contact } from '@/models/Contact'
-
-@Component
-export default class ContactIdPage extends Vue {
-  private contact: Contact = Contact.createEmpty()
-  private contactBackup: Contact | null = null
-
-  private editMode: boolean = false
-
-  created () {
-    this.initializeAppBar()
-    this.registerButtonActions()
-  }
-
-  mounted () {
-    this.loadContact().then((contact) => {
-      if (contact === null) {
-        this.$store.commit('appbar/setTitle', 'Add Contact')
-      }
-    }).catch(() => {
-      this.$store.commit('snackbar/setError', 'Loading contact failed')
-    })
-  }
-
-  beforeDestroy () {
-    this.unregisterButtonActions()
-    this.$store.dispatch('appbar/setDefaults')
-  }
-
-  registerButtonActions () {
-    this.$nuxt.$on('AppBarEditModeContent:save-btn-click', () => {
-      this.save().then(() => {
-        this.$store.commit('snackbar/setSuccess', 'Save successful')
-      }).catch(() => {
-        this.$store.commit('snackbar/setError', 'Save failed')
-      })
-    })
-    this.$nuxt.$on('AppBarEditModeContent:cancel-btn-click', () => {
-      this.cancel()
-    })
-  }
-
-  unregisterButtonActions () {
-    this.$nuxt.$off('AppBarEditModeContent:save-btn-click')
-    this.$nuxt.$off('AppBarEditModeContent:cancel-btn-click')
-  }
-
-  initializeAppBar () {
-    this.$store.dispatch('appbar/init', {
-      tabs: [],
-      title: 'Contacts',
-      saveBtnHidden: true,
-      cancelBtnHidden: true
-    })
-  }
-
-  loadContact (): Promise<Contact|null> {
-    return new Promise((resolve, reject) => {
-      const contactId = this.$route.params.id
-      if (!contactId) {
-        this.createBackup()
-        this.editMode = true && this.isLoggedIn
-        resolve(null)
-        return
-      }
-      this.editMode = false
-      this.$api.contacts.findById(contactId).then((foundContact) => {
-        this.contact = foundContact
-        resolve(foundContact)
-      }).catch((error) => {
-        reject(error)
-      })
-    })
-  }
-
-  createBackup () {
-    this.contactBackup = Contact.createFromObject(this.contact)
-  }
-
-  restoreBackup () {
-    if (!this.contactBackup) {
-      return
-    }
-    this.contact = this.contactBackup
-    this.contactBackup = null
-  }
-
-  save (): Promise<Contact|null> {
-    return new Promise((resolve, reject) => {
-      this.$api.contacts.save(this.contact).then((savedContact) => {
-        this.contact = savedContact
-        this.contactBackup = null
-        this.editMode = false
-        if (!this.$route.params.id && savedContact.id) {
-          this.setUrlWithNewId(savedContact.id)
-          // and we set the parameter so that we don't and up with
-          // multiple ids in the url
-          this.$route.params.id = savedContact.id
-        }
-        resolve(savedContact)
-      }).catch((error) => {
-        reject(error)
-      })
-    })
-  }
-
-  setUrlWithNewId (id: string) {
-    const oldUrl = this.$route.path
-    const newUrl = oldUrl + (oldUrl.endsWith('/') ? '' : '/') + id
-
-    history.pushState({}, '', newUrl)
-  }
-
-  cancel () {
-    this.restoreBackup()
-    if (this.contact.id) {
-      this.editMode = false
-    } else {
-      this.$router.push('search/contacts')
-    }
-  }
-
-  onEditButtonClick () {
-    this.createBackup()
-    this.editMode = true && this.isLoggedIn
-  }
-
-  get readonly () {
-    return !this.editMode
-  }
-
-  @Watch('contact', { immediate: true, deep: true })
-  onContactChanged (val: Contact) {
-    if (val.id) {
-      const fullName = this.getFullName(val)
-      this.$store.commit('appbar/setTitle', fullName || 'Add contact')
-    }
-  }
-
-  @Watch('editMode', { immediate: true, deep: true })
-  onEditModeChange (editMode: boolean) {
-    this.$store.commit('appbar/setSaveBtnHidden', !editMode)
-    this.$store.commit('appbar/setCancelBtnHidden', !editMode)
-  }
-
-  getFullName (contact: Contact) : string {
-    return contact.givenName + ' ' + contact.familyName
-  }
-
-  get title () : string {
-    const fullName = this.getFullName(this.contact).trim()
-    if (fullName) {
-      return 'Contact: ' + fullName
-    }
-    return 'Contact'
-  }
-
-  get isLoggedIn () {
-    return this.$store.getters['oidc/isAuthenticated']
-  }
-}
-</script>
diff --git a/pages/contacts/new.vue b/pages/contacts/new.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0c30be618e873592a23a582106be74db9abe91cf
--- /dev/null
+++ b/pages/contacts/new.vue
@@ -0,0 +1,152 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020-2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <div>
+    <ProgressIndicator
+      v-model="isLoading"
+      dark
+    />
+    <v-card
+      flat
+    >
+      <v-card-actions>
+        <v-spacer />
+        <v-btn
+          v-if="isLoggedIn"
+          small
+          text
+          nuxt
+          to="/search/contacts"
+        >
+          cancel
+        </v-btn>
+        <v-btn
+          v-if="isLoggedIn"
+          color="green"
+          small
+          @click="onSaveButtonClicked"
+        >
+          create
+        </v-btn>
+      </v-card-actions>
+      <ContactBasicDataForm
+        ref="basicForm"
+        v-model="contact"
+      />
+      <v-card-actions>
+        <v-spacer />
+        <v-btn
+          v-if="isLoggedIn"
+          small
+          text
+          nuxt
+          to="/search/contacts"
+        >
+          cancel
+        </v-btn>
+        <v-btn
+          v-if="isLoggedIn"
+          color="green"
+          small
+          @click="onSaveButtonClicked"
+        >
+          create
+        </v-btn>
+      </v-card-actions>
+    </v-card>
+  </div>
+</template>
+
+<style lang="scss">
+@import "@/assets/styles/_forms.scss";
+</style>
+
+<script lang="ts">
+import { Component, mixins } from 'nuxt-property-decorator'
+
+import { Rules } from '@/mixins/Rules'
+
+import { Contact } from '@/models/Contact'
+
+import ContactBasicDataForm from '@/components/ContactBasicDataForm.vue'
+import ProgressIndicator from '@/components/ProgressIndicator.vue'
+
+@Component({
+  components: {
+    ContactBasicDataForm,
+    ProgressIndicator
+  }
+})
+export default class ContactNewPage extends mixins(Rules) {
+  private numberOfTabs: number = 1
+
+  private contact: Contact = new Contact()
+  private isLoading: boolean = false
+
+  mounted () {
+    this.initializeAppBar()
+  }
+
+  beforeDestroy () {
+    this.$store.dispatch('appbar/setDefaults')
+  }
+
+  onSaveButtonClicked (): void {
+    if (!(this.$refs.basicForm as Vue & { validateForm: () => boolean }).validateForm()) {
+      this.$store.commit('snackbar/setError', 'Please correct your input')
+      return
+    }
+    if (!this.isLoggedIn) {
+      this.$store.commit('snackbar/setError', 'You need to be logged in to save the contact')
+      return
+    }
+    this.isLoading = true
+    this.$api.contacts.save(this.contact).then((savedContact) => {
+      this.isLoading = false
+      this.$store.commit('snackbar/setSuccess', 'Contact created')
+      this.$router.push('/contacts/' + savedContact.id + '')
+    }).catch((_error) => {
+      this.isLoading = false
+      this.$store.commit('snackbar/setError', 'Save failed')
+    })
+  }
+
+  initializeAppBar () {
+    this.$store.dispatch('appbar/init', {
+      title: 'Add Contact'
+    })
+  }
+
+  get isLoggedIn () {
+    return this.$store.getters['oidc/isAuthenticated']
+  }
+}
+</script>
diff --git a/pages/search/contacts.vue b/pages/search/contacts.vue
index 26c8dc4cf5a1ee7a4e8dee752fe13f94596fc678..d2a013ba84c5fe680d30fce28b0e307737a26840 100644
--- a/pages/search/contacts.vue
+++ b/pages/search/contacts.vue
@@ -298,7 +298,7 @@ permissions and limitations under the Licence.
       fab
       fixed
       right
-      to="/contacts"
+      to="/contacts/new"
     >
       <v-icon>
         mdi-plus