diff --git a/aleksis/core/assets/app/apollo.js b/aleksis/core/assets/app/apollo.js
index 5d0860f7c8ad5f5d81994684b353e3c824ea9cd9..84c47c6bb5278a1a811900e168c02aa820ee9a7d 100644
--- a/aleksis/core/assets/app/apollo.js
+++ b/aleksis/core/assets/app/apollo.js
@@ -99,7 +99,6 @@ const links = [
   }),
 ];
 
-
 /** Upstream Apollo GraphQL client */
 const apolloClient = new ApolloClient({
   typeDefs,
diff --git a/aleksis/core/assets/components/app/AccountMenu.vue b/aleksis/core/assets/components/app/AccountMenu.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d70de72bca3ca9e2ee7e54980398ed55a3f228e6
--- /dev/null
+++ b/aleksis/core/assets/components/app/AccountMenu.vue
@@ -0,0 +1,73 @@
+<template>
+  <v-menu offset-y>
+    <template #activator="{ on, attrs }">
+      <v-avatar v-bind="attrs" v-on="on">
+        <img
+          v-if="
+            systemProperties.sitePreferences.accountPersonPreferPhoto &&
+            whoAmI.person.photo &&
+            whoAmI.person.photo.url
+          "
+          :src="whoAmI.person.photo.url"
+          :alt="whoAmI.person.fullName"
+          :title="whoAmI.person.fullName"
+        />
+        <img
+          v-else-if="whoAmI.person.avatarUrl"
+          :src="whoAmI.person.avatarUrl"
+          :alt="whoAmI.person.fullName + '(' + $t('person.avatar') + ')'"
+          :title="whoAmI.person.fullName + '(' + $t('person.avatar') + ')'"
+        />
+        <v-icon v-else>mdi-person</v-icon>
+      </v-avatar>
+    </template>
+    <v-list>
+      <v-subheader>
+        {{
+          $t(
+            whoAmI && whoAmI.isImpersonate
+              ? "person.impersonation.impersonating"
+              : "person.logged_in_as"
+          )
+        }}
+        {{ whoAmI.person.fullName ? whoAmI.person.fullName : whoAmI.username }}
+      </v-subheader>
+      <v-list-item
+        v-if="whoAmI && whoAmI.isImpersonate"
+        :to="{ name: 'impersonate.stop', query: { next: $route.path } }"
+      >
+        <v-list-item-icon>
+          <v-icon> mdi-stop </v-icon>
+        </v-list-item-icon>
+        <v-list-item-title>
+          {{ $t("person.impersonation.stop") }}
+        </v-list-item-title>
+      </v-list-item>
+      <div v-for="menuItem in accountMenu" :key="menuItem.name">
+        <v-divider v-if="menuItem.divider"></v-divider>
+        <v-list-item
+          :to="{ name: menuItem.name }"
+          :target="menuItem.newTab ? '_blank' : '_self'"
+        >
+          <v-list-item-icon>
+            <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon>
+          </v-list-item-icon>
+          <v-list-item-title>{{ $t(menuItem.titleKey) }} </v-list-item-title>
+        </v-list-item>
+      </div>
+    </v-list>
+  </v-menu>
+</template>
+
+<script>
+export default {
+  name: "AccountMenu",
+  props: {
+    accountMenu: Array,
+    systemProperties: Object,
+    whoAmI: Object,
+  },
+};
+</script>
+
+<style></style>
diff --git a/aleksis/core/assets/components/app/App.vue b/aleksis/core/assets/components/app/App.vue
index baf2e69d7d22eef7f1e218926475d99f2340eca3..9c114d06c93947d41e07d1973627c373540063de 100644
--- a/aleksis/core/assets/components/app/App.vue
+++ b/aleksis/core/assets/components/app/App.vue
@@ -9,84 +9,11 @@
   <v-app v-cloak>
     <splash v-if="$apollo.loading && !systemProperties" splash />
     <div v-else>
-      <v-navigation-drawer app v-model="drawer">
-        <v-list nav dense shaped>
-          <v-list-item class="logo">
-            <a
-              id="logo-container"
-              @click="$router.push({ name: 'dashboard' })"
-              class="brand-logo"
-            >
-              <brand-logo
-                :site-preferences="systemProperties.sitePreferences"
-              />
-            </a>
-          </v-list-item>
-          <v-list-item class="search">
-            <sidenav-search />
-          </v-list-item>
-          <v-list-item-group :value="$route.name" v-if="sideNavMenu">
-            <div v-for="menuItem in sideNavMenu" :key="menuItem.name">
-              <v-list-group
-                v-if="menuItem.subMenu.length > 0"
-                href="#!"
-                :prepend-icon="menuItem.icon"
-                :value="$route.matched.slice(-2).shift().name === menuItem.name"
-              >
-                <template #activator>
-                  <v-list-item-title
-                    >{{ $t(menuItem.titleKey) }}
-                  </v-list-item-title>
-                </template>
-                <v-list-item
-                  v-for="subMenuItem in menuItem.subMenu"
-                  exact
-                  :to="{ name: subMenuItem.name }"
-                  :target="subMenuItem.newTab ? '_blank' : '_self'"
-                  :key="subMenuItem.name"
-                  :value="subMenuItem.name"
-                >
-                  <v-list-item-icon>
-                    <v-icon v-if="subMenuItem.icon"
-                      >{{ subMenuItem.icon }}
-                    </v-icon>
-                  </v-list-item-icon>
-                  <v-list-item-title
-                    >{{ $t(subMenuItem.titleKey) }}
-                  </v-list-item-title>
-                </v-list-item>
-              </v-list-group>
-              <v-list-item
-                v-else
-                exact
-                :to="{ name: menuItem.name }"
-                :target="menuItem.newTab ? '_blank' : '_self'"
-                :value="menuItem.name"
-              >
-                <v-list-item-icon>
-                  <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon>
-                </v-list-item-icon>
-                <v-list-item-title>{{
-                  $t(menuItem.titleKey)
-                }}</v-list-item-title>
-              </v-list-item>
-            </div>
-          </v-list-item-group>
-          <v-list-item v-else>
-            <v-skeleton-loader class="ma-2" type="list-item-avatar" />
-          </v-list-item>
-        </v-list>
-
-        <template #append>
-          <div class="pa-4 d-flex justify-center align-center">
-            <v-spacer />
-            <language-form
-              :available-languages="systemProperties.availableLanguages"
-            />
-            <v-spacer />
-          </div>
-        </template>
-      </v-navigation-drawer>
+      <side-nav
+        v-model="drawer"
+        :system-properties="systemProperties"
+        :side-nav-menu="sideNavMenu"
+      ></side-nav>
       <v-app-bar
         app
         :color="$vuetify.theme.dark ? undefined : 'primary white--text'"
@@ -120,74 +47,11 @@
         </v-btn>
         <div v-if="whoAmI && whoAmI.isAuthenticated" class="d-flex">
           <notification-list v-if="!whoAmI.person.isDummy" />
-          <v-menu offset-y>
-            <template #activator="{ on, attrs }">
-              <v-avatar v-bind="attrs" v-on="on">
-                <img
-                  v-if="
-                    systemProperties.sitePreferences.accountPersonPreferPhoto &&
-                    whoAmI.person.photo &&
-                    whoAmI.person.photo.url
-                  "
-                  :src="whoAmI.person.photo.url"
-                  :alt="whoAmI.person.fullName"
-                  :title="whoAmI.person.fullName"
-                />
-                <img
-                  v-else-if="whoAmI.person.avatarUrl"
-                  :src="whoAmI.person.avatarUrl"
-                  :alt="
-                    whoAmI.person.fullName + '(' + $t('person.avatar') + ')'
-                  "
-                  :title="
-                    whoAmI.person.fullName + '(' + $t('person.avatar') + ')'
-                  "
-                />
-                <v-icon v-else>mdi-person</v-icon>
-              </v-avatar>
-            </template>
-            <v-list>
-              <v-subheader>
-                {{
-                  $t(
-                    whoAmI && whoAmI.isImpersonate
-                      ? "person.impersonation.impersonating"
-                      : "person.logged_in_as"
-                  )
-                }}
-                {{
-                  whoAmI.person.fullName
-                    ? whoAmI.person.fullName
-                    : whoAmI.username
-                }}
-              </v-subheader>
-              <v-list-item
-                v-if="whoAmI && whoAmI.isImpersonate"
-                :to="{ name: 'impersonate.stop', query: { next: $route.path } }"
-              >
-                <v-list-item-icon>
-                  <v-icon> mdi-stop </v-icon>
-                </v-list-item-icon>
-                <v-list-item-title>
-                  {{ $t("person.impersonation.stop") }}
-                </v-list-item-title>
-              </v-list-item>
-              <div v-for="menuItem in accountMenu" :key="menuItem.name">
-                <v-divider v-if="menuItem.divider"></v-divider>
-                <v-list-item
-                  :to="{ name: menuItem.name }"
-                  :target="menuItem.newTab ? '_blank' : '_self'"
-                >
-                  <v-list-item-icon>
-                    <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon>
-                  </v-list-item-icon>
-                  <v-list-item-title
-                    >{{ $t(menuItem.titleKey) }}
-                  </v-list-item-title>
-                </v-list-item>
-              </div>
-            </v-list>
-          </v-menu>
+          <account-menu
+            :account-menu="accountMenu"
+            :system-properties="systemProperties"
+            :who-am-i="whoAmI"
+          ></account-menu>
         </div>
       </v-app-bar>
       <v-main>
@@ -342,12 +206,11 @@
 
 <script>
 import BroadcastChannelNotification from "./BroadcastChannelNotification.vue";
-import LanguageForm from "./LanguageForm.vue";
+import AccountMenu from "./AccountMenu.vue";
 import NotificationList from "../notifications/NotificationList.vue";
-import SidenavSearch from "./SidenavSearch.vue";
 import CeleryProgressBottom from "../celery_progress/CeleryProgressBottom.vue";
 import Splash from "./Splash.vue";
-import BrandLogo from "./BrandLogo.vue";
+import SideNav from "./SideNav.vue";
 import SnackbarItem from "./SnackbarItem.vue";
 
 import gqlWhoAmI from "./whoAmI.graphql";
@@ -411,13 +274,12 @@ export default {
   },
   name: "App",
   components: {
-    BrandLogo,
+    AccountMenu,
     BroadcastChannelNotification,
-    LanguageForm,
     NotificationList,
-    SidenavSearch,
     CeleryProgressBottom,
     Splash,
+    SideNav,
     SnackbarItem,
   },
   mixins: [useRegisterSWMixin, offlineMixin, menusMixin],
diff --git a/aleksis/core/assets/components/app/LanguageForm.vue b/aleksis/core/assets/components/app/LanguageForm.vue
index 7757b9f46622ad0ba3e0158be75f85220a5f0454..355f719e45eb6e77f2ca6e439687986cbbf788de 100644
--- a/aleksis/core/assets/components/app/LanguageForm.vue
+++ b/aleksis/core/assets/components/app/LanguageForm.vue
@@ -8,6 +8,7 @@
     menu-props="auto"
     outlined
     color="primary"
+    hide-details="auto"
     single-line
     return-object
     dense
diff --git a/aleksis/core/assets/components/app/SideNav.vue b/aleksis/core/assets/components/app/SideNav.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e3452851ffa0f79768c02163177acc98e63114da
--- /dev/null
+++ b/aleksis/core/assets/components/app/SideNav.vue
@@ -0,0 +1,95 @@
+<template>
+  <v-navigation-drawer app>
+    <v-list nav dense shaped>
+      <v-list-item class="logo">
+        <a
+          id="logo-container"
+          @click="$router.push({ name: 'dashboard' })"
+          class="brand-logo"
+        >
+          <brand-logo :site-preferences="systemProperties.sitePreferences" />
+        </a>
+      </v-list-item>
+      <v-list-item class="search">
+        <sidenav-search />
+      </v-list-item>
+      <v-list-item-group :value="$route.name" v-if="sideNavMenu">
+        <div v-for="menuItem in sideNavMenu" :key="menuItem.name">
+          <v-list-group
+            v-if="menuItem.subMenu.length > 0"
+            href="#!"
+            :prepend-icon="menuItem.icon"
+            :value="$route.matched.slice(-2).shift().name === menuItem.name"
+          >
+            <template #activator>
+              <v-list-item-title
+                >{{ $t(menuItem.titleKey) }}
+              </v-list-item-title>
+            </template>
+            <v-list-item
+              v-for="subMenuItem in menuItem.subMenu"
+              exact
+              :to="{ name: subMenuItem.name }"
+              :target="subMenuItem.newTab ? '_blank' : '_self'"
+              :key="subMenuItem.name"
+              :value="subMenuItem.name"
+            >
+              <v-list-item-icon>
+                <v-icon v-if="subMenuItem.icon">{{ subMenuItem.icon }} </v-icon>
+              </v-list-item-icon>
+              <v-list-item-title
+                >{{ $t(subMenuItem.titleKey) }}
+              </v-list-item-title>
+            </v-list-item>
+          </v-list-group>
+          <v-list-item
+            v-else
+            exact
+            :to="{ name: menuItem.name }"
+            :target="menuItem.newTab ? '_blank' : '_self'"
+            :value="menuItem.name"
+          >
+            <v-list-item-icon>
+              <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon>
+            </v-list-item-icon>
+            <v-list-item-title>{{ $t(menuItem.titleKey) }}</v-list-item-title>
+          </v-list-item>
+        </div>
+      </v-list-item-group>
+      <template v-else>
+        <v-skeleton-loader class="ma-2" type="list-item-avatar@5" />
+      </template>
+    </v-list>
+
+    <template #append>
+      <div class="pa-4 d-flex justify-center align-center">
+        <v-spacer />
+        <language-form
+          :available-languages="systemProperties.availableLanguages"
+        />
+        <v-spacer />
+      </div>
+    </template>
+  </v-navigation-drawer>
+</template>
+
+<script>
+import BrandLogo from "./BrandLogo.vue";
+import LanguageForm from "./LanguageForm.vue";
+import SidenavSearch from "./SidenavSearch.vue";
+
+export default {
+  name: "SideNav",
+  components: {
+    BrandLogo,
+    LanguageForm,
+    SidenavSearch,
+  },
+  props: {
+    sideNavMenu: Array,
+    systemProperties: Object,
+  },
+};
+</script>
+
+<style scoped></style>
diff --git a/aleksis/core/assets/components/person/AdditionalImage.vue b/aleksis/core/assets/components/person/AdditionalImage.vue
index 943c8a198cd8373d0d4f42b75613c7b527b5629f..f483705806bb5eb718ccf136a4773b4a5bac7130 100644
--- a/aleksis/core/assets/components/person/AdditionalImage.vue
+++ b/aleksis/core/assets/components/person/AdditionalImage.vue
@@ -13,14 +13,7 @@
       </v-card>
     </template>
     <v-sheet
-      class="
-        d-flex
-        justify-center
-        align-center
-        flex-column
-        text-center
-        transparent
-      "
+      class="d-flex justify-center align-center flex-column text-center transparent"
     >
       <v-img
         :src="src"
diff --git a/aleksis/core/assets/index.js b/aleksis/core/assets/index.js
index d681aca2f7a7e48e9a446baf5ab248126e991102..459978ed1d533e73ea5b03bf8f8ef700cefcfeb7 100644
--- a/aleksis/core/assets/index.js
+++ b/aleksis/core/assets/index.js
@@ -1,6 +1,6 @@
 /*
  * Main entrypoint of AlekSIS0-ore.
- * 
+ *
  * This script sets up all necessary Vue plugins and defines the Vue app.
  */
 
diff --git a/aleksis/core/assets/mixins/menus.js b/aleksis/core/assets/mixins/menus.js
index a7bace279eac72bf5b724044061d23d1ba4d30a0..8afed7f424ebf1bd4b33b316dc311f14831161fc 100644
--- a/aleksis/core/assets/mixins/menus.js
+++ b/aleksis/core/assets/mixins/menus.js
@@ -2,7 +2,7 @@ import gqlCustomMenu from "../components/app/customMenu.graphql";
 
 /**
  * Vue mixin containing menu generation code.
- * 
+ *
  * Only used by main App component, but factored out for readability.
  */
 const menusMixin = {
diff --git a/aleksis/core/assets/mixins/offline.js b/aleksis/core/assets/mixins/offline.js
index 40b10b3b41ba1a8d15585e37ef7e05789a534db2..7bdd551a33a1361120a41b448cd7aa640935e40d 100644
--- a/aleksis/core/assets/mixins/offline.js
+++ b/aleksis/core/assets/mixins/offline.js
@@ -2,12 +2,12 @@ import gqlPing from "../components/app/ping.graphql";
 
 /**
  * Mixin for handling of offline state / background queries.
- * 
+ *
  * This handles three scenarios:
  *   - The navigator reports that it is in offline mode
  *   - The global offline flag was set due to network errors from queries
  *   - The navigator reports the page to be invisible
- * 
+ *
  * The main goal is to save bandwidth, energy and server load in error
  * conditions, or when the page is not in focus. This is achieved by a
  * fallback strategy, where all background queries are stopped in offline
diff --git a/aleksis/core/assets/routeValidators.js b/aleksis/core/assets/routeValidators.js
index af9a2ad41c4c98e6c62c116227a4ba7c4fef7d05..1df949d0349412be9a50f81c53837dab7210d80b 100644
--- a/aleksis/core/assets/routeValidators.js
+++ b/aleksis/core/assets/routeValidators.js
@@ -1,6 +1,6 @@
 /**
  * Check whether the user is logged in on the AlekSIS server.
- * 
+ *
  * @param {Object} whoAmI The person object as returned by the whoAmI query
  * @returns true if the user is logged in, false if not
  */
diff --git a/aleksis/core/assets/routes.js b/aleksis/core/assets/routes.js
index d969797e8e591363c4fcc5d24c0b7e4cd1fc5ab7..0a9572671ca4fe72ffddd4ed3e46704b82782a8b 100644
--- a/aleksis/core/assets/routes.js
+++ b/aleksis/core/assets/routes.js
@@ -1,6 +1,6 @@
 /*
  * Vue router definitions for all of AlekSIS.
- * 
+ *
  * This module defines the routes of AlekSIS-Core and also loads
  * and adds all routes from known apps.
  */
diff --git a/aleksis/core/vite.config.js b/aleksis/core/vite.config.js
index 3f18652120fdd11e74932ed9a0c598a679d8b36b..7954a54fb9d9327f9a5a2a38510d064b4c41d099 100644
--- a/aleksis/core/vite.config.js
+++ b/aleksis/core/vite.config.js
@@ -147,7 +147,9 @@ export default defineConfig({
       base: "/",
       workbox: {
         navigateFallback: "/",
-        navigateFallbackAllowlist: [new RegExp("^/(?!(django|admin|graphql|__icons__))[^.]*$")],
+        navigateFallbackAllowlist: [
+          new RegExp("^/(?!(django|admin|graphql|__icons__))[^.]*$"),
+        ],
         additionalManifestEntries: ["/", "/django/offline/"],
         inlineWorkboxRuntime: true,
         modifyURLPrefix: {
@@ -156,7 +158,9 @@ export default defineConfig({
         globPatterns: ["**/*.{js,css,eot,woff,woff2,ttf}"],
         runtimeCaching: [
           {
-            urlPattern: new RegExp("^/(?!(django|admin|graphql|__icons__))[^.]*$"),
+            urlPattern: new RegExp(
+              "^/(?!(django|admin|graphql|__icons__))[^.]*$"
+            ),
             handler: "CacheFirst",
           },
           {