diff --git a/aleksis/core/frontend/collections.js b/aleksis/core/frontend/collections.js
index b25b6e2350efcbe3a621c950fda632e8f875ea9d..98cf666d98980464b8bd66393b62db26cb73025c 100644
--- a/aleksis/core/frontend/collections.js
+++ b/aleksis/core/frontend/collections.js
@@ -13,6 +13,22 @@ export const collections = [
       },
     ],
   },
+  {
+    name: "groupActions",
+    type: Object,
+  },
+  {
+    name: "personWidgets",
+    type: Object,
+  },
 ];
 
-export const collectionItems = {};
+export const collectionItems = {
+  coreGroupActions: [
+    {
+      key: "core-delete-group-action",
+      component: () => import("./components/group/actions/DeleteGroup.vue"),
+      isActive: (group) => group.canDelete || false,
+    },
+  ],
+};
diff --git a/aleksis/core/frontend/components/generic/ButtonMenu.vue b/aleksis/core/frontend/components/generic/ButtonMenu.vue
index 431407d27ff78654c13aa548d27132dd74b5c0dd..f1ef728503695747b278003d85e405a76787a65d 100644
--- a/aleksis/core/frontend/components/generic/ButtonMenu.vue
+++ b/aleksis/core/frontend/components/generic/ButtonMenu.vue
@@ -1,5 +1,9 @@
 <template>
-  <v-menu transition="slide-y-transition" offset-y>
+  <v-menu
+    transition="slide-y-transition"
+    offset-y
+    :close-on-content-click="closeOnContentClick"
+  >
     <template #activator="{ on, attrs }">
       <slot name="activator" v-bind="{ on, attrs }">
         <v-btn outlined text v-bind="attrs" v-on="on">
@@ -31,6 +35,11 @@ export default {
       required: false,
       default: "",
     },
+    closeOnContentClick: {
+      type: Boolean,
+      required: false,
+      default: true,
+    },
   },
 };
 </script>
diff --git a/aleksis/core/frontend/components/generic/InlineCRUDList.vue b/aleksis/core/frontend/components/generic/InlineCRUDList.vue
index 3341bfb546ba63d65a3f64996099f4c8e0f06760..24a4af0fc0e50cab24ce2d2ad0e81dd428845cb6 100644
--- a/aleksis/core/frontend/components/generic/InlineCRUDList.vue
+++ b/aleksis/core/frontend/components/generic/InlineCRUDList.vue
@@ -55,6 +55,10 @@
         <slot name="additionalActions" />
       </template>
 
+      <template #actions="actions">
+        <slot name="actions" v-bind="actions" />
+      </template>
+
       <!-- customizable headers -->
       <template
         v-for="(_header, idx) in $attrs.headers"
diff --git a/aleksis/core/frontend/components/generic/chips/CounterChip.vue b/aleksis/core/frontend/components/generic/chips/CounterChip.vue
index 04ae1b4766781e9f8a108f95373a4622ad51fa54..c7e39eb26678e564b44f6639063ad4c0f8658afb 100644
--- a/aleksis/core/frontend/components/generic/chips/CounterChip.vue
+++ b/aleksis/core/frontend/components/generic/chips/CounterChip.vue
@@ -1,7 +1,7 @@
 <template>
   <v-chip v-bind="$attrs" v-on="$listeners">
     <v-avatar :left="!onlyShowCount" v-if="count !== null">
-      {{ count }}
+      {{ $n(count) }}
     </v-avatar>
     <slot v-if="!onlyShowCount" />
   </v-chip>
diff --git a/aleksis/core/frontend/components/group/GroupActions.vue b/aleksis/core/frontend/components/group/GroupActions.vue
index 72d21f44bd1b1b049608cad2fade2fd0a5381170..b9f5d606999822f4fbd4bde66dfcb7a0de18de11 100644
--- a/aleksis/core/frontend/components/group/GroupActions.vue
+++ b/aleksis/core/frontend/components/group/GroupActions.vue
@@ -6,52 +6,32 @@
       :to="{ name: 'core.editGroup', params: { id: group.id } }"
     />
 
-    <delete-button
-      v-if="group.canDelete"
-      @click="showDeleteConfirm = true"
-      outlined
-      text
-      color="error"
-    />
-
-    <delete-dialog
-      v-model="showDeleteConfirm"
-      :gql-delete-mutation="deleteMutation"
-      item-attribute="name"
-      :items="[group]"
-      @save="
-        $router.push({
-          name: 'core.groups',
-        })
-      "
-    >
-      <template #title>
-        {{ $t("group.confirm_delete") }}
-      </template>
-    </delete-dialog>
+    <button-menu :close-on-content-click="false" v-if="actions.length">
+      <component
+        :is="action.component"
+        v-for="action in actions"
+        :key="action.key"
+        :group="group"
+      />
+    </button-menu>
   </div>
 </template>
 
 <script>
-import { deleteGroups } from "./groups.graphql";
-import DeleteDialog from "../generic/dialogs/DeleteDialog.vue";
-import DeleteButton from "../generic/buttons/DeleteButton.vue";
 import EditButton from "../generic/buttons/EditButton.vue";
+import { collections } from "aleksisAppImporter";
+import groupActionsMixin from "./actions/groupActionsMixin";
 
 export default {
   name: "GroupActions",
-  components: { EditButton, DeleteButton, DeleteDialog },
-  props: {
-    group: {
-      type: Object,
-      required: true,
+  components: { EditButton },
+  mixins: [groupActionsMixin],
+  computed: {
+    actions() {
+      return collections.coreGroupActions.items.filter((action) =>
+        action.isActive.call(this, this.group),
+      );
     },
   },
-  data() {
-    return {
-      showDeleteConfirm: false,
-      deleteMutation: deleteGroups,
-    };
-  },
 };
 </script>
diff --git a/aleksis/core/frontend/components/group/actions/DeleteGroup.vue b/aleksis/core/frontend/components/group/actions/DeleteGroup.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dc18a0a001b7956cf0050f84be783b796e055ab9
--- /dev/null
+++ b/aleksis/core/frontend/components/group/actions/DeleteGroup.vue
@@ -0,0 +1,48 @@
+<script>
+import { deleteGroups } from "../groups.graphql";
+import DeleteDialog from "../../generic/dialogs/DeleteDialog.vue";
+import groupActionsMixin from "./groupActionsMixin";
+
+export default {
+  name: "DeleteGroup",
+  components: { DeleteDialog },
+  mixins: [groupActionsMixin],
+  data() {
+    return {
+      showDeleteConfirm: false,
+      deleteMutation: deleteGroups,
+    };
+  },
+};
+</script>
+
+<template>
+  <v-list-item @click="showDeleteConfirm = true" class="error--text">
+    <v-list-item-icon>
+      <v-icon color="error">$deleteContent</v-icon>
+    </v-list-item-icon>
+    <v-list-item-content>
+      <v-list-item-title>
+        {{ $t("actions.delete") }}
+      </v-list-item-title>
+    </v-list-item-content>
+
+    <delete-dialog
+      v-model="showDeleteConfirm"
+      :gql-delete-mutation="deleteMutation"
+      item-attribute="name"
+      :items="[group]"
+      @save="
+        $router.push({
+          name: 'core.groups',
+        })
+      "
+    >
+      <template #title>
+        {{ $t("group.confirm_delete") }}
+      </template>
+    </delete-dialog>
+  </v-list-item>
+</template>
+
+<style scoped></style>
diff --git a/aleksis/core/frontend/components/group/actions/groupActionsMixin.js b/aleksis/core/frontend/components/group/actions/groupActionsMixin.js
new file mode 100644
index 0000000000000000000000000000000000000000..831d2514ff93792f7b005526042c1007fec10987
--- /dev/null
+++ b/aleksis/core/frontend/components/group/actions/groupActionsMixin.js
@@ -0,0 +1,8 @@
+export default {
+  props: {
+    group: {
+      type: Object,
+      required: true,
+    },
+  },
+};
diff --git a/aleksis/core/frontend/components/person/PersonOverview.vue b/aleksis/core/frontend/components/person/PersonOverview.vue
index 90933bf625804e9ab0a6811d843f15e451d392f5..ac23625d38dcd34af93011bf6365721b3bf8df51 100644
--- a/aleksis/core/frontend/components/person/PersonOverview.vue
+++ b/aleksis/core/frontend/components/person/PersonOverview.vue
@@ -228,15 +228,56 @@
             lg="4"
             v-if="person.memberOf.length || person.ownerOf.length"
           >
-            <v-card v-if="person.memberOf.length" class="mb-6">
+            <v-card>
               <v-card-title>{{ $t("group.title_plural") }}</v-card-title>
-              <group-collection :groups="person.memberOf" />
-            </v-card>
-            <v-card v-if="person.ownerOf.length">
-              <v-card-title>{{ $t("group.ownership") }}</v-card-title>
-              <group-collection :groups="person.ownerOf" />
+              <v-list-group
+                :disabled="person.memberOf.length === 0"
+                :append-icon="person.memberOf.length === 0 ? null : undefined"
+              >
+                <template #activator>
+                  <v-list-item-icon>
+                    <v-icon>mdi-account-group-outline</v-icon>
+                  </v-list-item-icon>
+                  <v-list-item-title>{{
+                    $tc("group.member_of_n", person.memberOf.length)
+                  }}</v-list-item-title>
+                </template>
+                <group-collection :groups="person.memberOf" dense />
+              </v-list-group>
+              <v-list-group
+                :disabled="person.ownerOf.length === 0"
+                :append-icon="person.ownerOf.length === 0 ? null : undefined"
+              >
+                <template #activator>
+                  <v-list-item-icon>
+                    <v-icon>mdi-account-tie-hat-outline</v-icon>
+                  </v-list-item-icon>
+                  <v-list-item-title>{{
+                    $tc("group.owner_of_n", person.ownerOf.length)
+                  }}</v-list-item-title>
+                </template>
+                <group-collection :groups="person.ownerOf" dense />
+              </v-list-group>
             </v-card>
           </v-col>
+
+          <template v-for="widget in widgets">
+            <v-col
+              v-if="widget.shouldDisplay(person, currentSchoolTerm)"
+              v-bind="widget.colProps"
+              :key="widget.key"
+            >
+              <!-- Props defined in aleksis/core/frontend/mixins/personOverviewCardMixin.js -->
+              <component
+                :is="widget.component"
+                :person="person"
+                :school-term="currentSchoolTerm"
+                :maximized="widgetSlug === widget.key"
+                @maximize="maximizeWidget(widget.key)"
+                @minimize="minimizeWidgets()"
+              />
+            </v-col>
+          </template>
         </v-row>
       </detail-view>
     </template>
@@ -251,8 +292,11 @@ import PersonActions from "./PersonActions.vue";
 import PersonAvatarClickbox from "./PersonAvatarClickbox.vue";
 import PersonCollection from "./PersonCollection.vue";
 
+import gqlCurrentSchoolTerm from "../school_term/currentSchoolTerm.graphql";
 import gqlPersonOverview from "./personOverview.graphql";
 
+import { collections } from "aleksisAppImporter";
+
 export default {
   name: "PersonOverview",
   components: {
@@ -263,9 +307,15 @@ export default {
     PersonAvatarClickbox,
     PersonCollection,
   },
+  apollo: {
+    currentSchoolTerm: {
+      query: gqlCurrentSchoolTerm,
+    },
+  },
   data() {
     return {
       query: gqlPersonOverview,
+      currentSchoolTerm: null,
     };
   },
   props: {
@@ -274,6 +324,46 @@ export default {
       required: false,
       default: null,
     },
+    widgetSlug: {
+      type: String,
+      required: false,
+      default: "default",
+    },
+  },
+  methods: {
+    maximizeWidget(slug) {
+      if (this.widgetSlug !== slug) {
+        if (this.id) {
+          this.$router.push({
+            name: "core.personByIdWithSlug",
+            params: { id: this.id, widgetSlug: slug },
+          });
+        } else {
+          this.$router.push({
+            name: "core.personWithSlug",
+            params: { widgetSlug: slug },
+          });
+        }
+      }
+    },
+    minimizeWidgets() {
+      if (this.id) {
+        this.$router.push({
+          name: "core.personByIdWithSlug",
+          params: { id: this.id, widgetSlug: "default" },
+        });
+      } else {
+        this.$router.push({
+          name: "core.personWithSlug",
+          params: { widgetSlug: "default" },
+        });
+      }
+    },
+  },
+  computed: {
+    widgets() {
+      return collections.corePersonWidgets.items;
+    },
   },
 };
 </script>
diff --git a/aleksis/core/frontend/components/school_term/currentSchoolTerm.graphql b/aleksis/core/frontend/components/school_term/currentSchoolTerm.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..82a7741f54770fc1a7edb7d6ce5ebcf74ffbb1f5
--- /dev/null
+++ b/aleksis/core/frontend/components/school_term/currentSchoolTerm.graphql
@@ -0,0 +1,10 @@
+query currentSchoolTerm {
+  currentSchoolTerm {
+    id
+    name
+    dateStart
+    dateEnd
+    canEdit
+    canDelete
+  }
+}
diff --git a/aleksis/core/frontend/messages/de.json b/aleksis/core/frontend/messages/de.json
index 2b65e8f31ea122a0821dbc994c90a30e6662b84c..475c0ac5a0e05f7d27485b758f5a035bd51fdec7 100644
--- a/aleksis/core/frontend/messages/de.json
+++ b/aleksis/core/frontend/messages/de.json
@@ -246,6 +246,8 @@
     "ownership": "Gruppen-Eigentümerschaft",
     "parent_groups": "Ãœbergeordnete Gruppen",
     "parent_groups_n": "Keine übergeordneten Gruppen | {n} übergeordnete Gruppe | {n} übergeordnete Gruppen",
+    "member_of_n": "Keine Gruppenmitgliedschaften | Mitglied in einer Gruppe | Mitglied in {n} Gruppen",
+    "owner_of_n": "Keine Gruppeneigentümerschaften | Besitzt eine Gruppe | Besitzt {n} Gruppen",
     "properties": "Eigenschaften",
     "short_name": "Kurzname",
     "statistics": {
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index 6b581d999247c077890c5eb95d5590ceb6c7254e..bfbcb76074e92a1a14e11e02a7b6ea3bdbe814b0 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -198,6 +198,8 @@
     "child_groups_n": "No Child Groups | {n} Child Group | {n} Child Groups",
     "parent_groups": "Parent Groups",
     "parent_groups_n": "No Parent Groups | {n} Parent Group | {n} Parent Groups",
+    "member_of_n": "Not member in any group | Member of {n} group | Member of {n} groups",
+    "owner_of_n": "Not owner of any group | Owner of {n} group | Owner of {n} groups",
     "confirm_delete": "Do you really want to delete this group?",
     "statistics": {
       "title": "Statistics",
diff --git a/aleksis/core/frontend/mixins/personOverviewCardMixin.js b/aleksis/core/frontend/mixins/personOverviewCardMixin.js
index 1e00f2c5eaa027a9f29b48a62090d70089901e79..7da808c34ac839d8e2ce7170d960b957019d2a61 100644
--- a/aleksis/core/frontend/mixins/personOverviewCardMixin.js
+++ b/aleksis/core/frontend/mixins/personOverviewCardMixin.js
@@ -18,5 +18,19 @@ export default {
       required: false,
       default: null,
     },
+    /**
+     * Whether the current widget is maximized
+     */
+    maximized: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    emits: [
+      // When this is fired, the component can assume that the `maximized` prop will soon turn true
+      "maximize",
+      // Use this to signify a wanted closure
+      "minimize",
+    ],
   },
 };
diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js
index d4efe17320770bfef7d469614a97e46807069d80..0a8b2528d3c8a4f1b4786986a7adb0d3bf970f16 100644
--- a/aleksis/core/frontend/routes.js
+++ b/aleksis/core/frontend/routes.js
@@ -157,6 +157,15 @@ const routes = [
         },
         name: "core.invitePerson",
       },
+      {
+        path: "/persons/:id(\\d+)/:widgetSlug([^\\s!?\\/*#|]+)",
+        component: () => import("./components/person/PersonOverview.vue"),
+        props: true,
+        name: "core.personByIdWithSlug",
+        meta: {
+          titleKey: "person.page_title",
+        },
+      },
       {
         path: "/groups",
         component: () => import("./components/LegacyBaseTemplate.vue"),
@@ -623,9 +632,6 @@ const routes = [
   {
     path: "/person/",
     component: () => import("./components/person/PersonOverview.vue"),
-    props: {
-      byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-    },
     name: "core.person",
     meta: {
       inAccountMenu: true,
@@ -635,6 +641,15 @@ const routes = [
       permission: "core.view_account_rule",
     },
   },
+  {
+    path: "/person/:widgetSlug([^\\s!?\\/*#|]+)/",
+    component: () => import("./components/person/PersonOverview.vue"),
+    props: true,
+    name: "core.personWithSlug",
+    meta: {
+      permission: "core.view_account_rule",
+    },
+  },
   {
     path: "/preferences/person/",
     component: () => import("./components/LegacyBaseTemplate.vue"),
diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py
index d545cedb4d3f690f2b9bf11528c6cae569e92620..2132d599fd96c0c6e8f9f41880a0609253eccd46 100644
--- a/aleksis/core/schema/__init__.py
+++ b/aleksis/core/schema/__init__.py
@@ -18,6 +18,7 @@ from ..models import (
     PDFFile,
     Person,
     Room,
+    SchoolTerm,
     TaskUserAssignment,
 )
 from ..util.apps import AppConfig
@@ -115,6 +116,7 @@ class Query(graphene.ObjectType):
     room_by_id = graphene.Field(RoomType, id=graphene.ID())
 
     school_terms = FilterOrderList(SchoolTermType)
+    current_school_term = graphene.Field(SchoolTermType)
 
     holidays = FilterOrderList(HolidayType)
     calendar = graphene.Field(CalendarBaseType)
@@ -284,6 +286,13 @@ class Query(graphene.ObjectType):
     def resolve_calendar(root, info, **kwargs):
         return True
 
+    @staticmethod
+    def resolve_current_school_term(root, info, **kwargs):
+        if not has_person(info.context.user):
+            return None
+
+        return SchoolTerm.current
+
 
 class Mutation(graphene.ObjectType):
     delete_persons = PersonBatchDeleteMutation.Field()