From 212396a8ede9777a90b0930e6a468e925ae01048 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Fri, 17 Jan 2025 16:22:34 +0100
Subject: [PATCH] Add expand functionality for all day calendar events in
 calendar

---
 .../frontend/components/calendar/Calendar.vue | 84 ++++++++++++++++++-
 aleksis/core/frontend/css/global.scss         |  5 ++
 aleksis/core/frontend/messages/en.json        |  3 +-
 3 files changed, 90 insertions(+), 2 deletions(-)

diff --git a/aleksis/core/frontend/components/calendar/Calendar.vue b/aleksis/core/frontend/components/calendar/Calendar.vue
index 5f5c44c9c..d8fcdfe39 100644
--- a/aleksis/core/frontend/components/calendar/Calendar.vue
+++ b/aleksis/core/frontend/components/calendar/Calendar.vue
@@ -40,6 +40,44 @@
             :calendar-type="internalCalendarType"
           />
         </template>
+        <template
+          v-if="Object.keys(daysWithHiddenEvents).length"
+          #interval-header
+        >
+          <div
+            v-if="
+              !internalCalendarType === 'day' ||
+              Object.keys(daysWithHiddenEvents).includes(internalCalendarFocus)
+            "
+            class="d-flex justify-center align-end"
+            :style="{ height: '100%' }"
+          >
+            <v-btn
+              icon
+              class="ma-2"
+              @click="showAllAllDayEvents = !showAllAllDayEvents"
+            >
+              <v-icon>{{ showAllAllDayEventsButtonIcon }}</v-icon>
+            </v-btn>
+          </div>
+        </template>
+        <template #day-header="{ date }">
+          <template
+            v-if="
+              Object.keys(daysWithHiddenEvents).includes(date) &&
+              !showAllAllDayEvents
+            "
+          >
+            <v-spacer />
+            <div
+              class="v-event-more ml-1"
+              v-ripple
+              @click="showAllAllDayEvents = true"
+            >
+              {{ $tc("calendar.more_events", daysWithHiddenEvents[date]) }}
+            </div>
+          </template>
+        </template>
       </v-calendar>
       <component
         v-if="selectedEvent"
@@ -65,6 +103,8 @@ import {
 
 import { gqlCalendar, calendarDaysPreference } from "./calendar.graphql";
 
+import { Interval } from "luxon";
+
 export default {
   name: "Calendar",
   props: {
@@ -104,6 +144,11 @@ export default {
       required: false,
       default: "current",
     },
+    maxAllDayEvents: {
+      type: Number,
+      required: false,
+      default: 5,
+    },
   },
   data() {
     return {
@@ -139,6 +184,9 @@ export default {
           daysOfWeek: [1, 2, 3, 4, 5, 6, 0],
         },
       },
+
+      showAllAllDayEvents: false,
+      daysWithHiddenEvents: {},
     };
   },
   emits: ["changeCalendarType", "changeCalendarFocus", "selectEvent"],
@@ -164,7 +212,7 @@ export default {
       };
     },
     events() {
-      return this.calendar.calendarFeeds
+      let events = this.calendar.calendarFeeds
         .filter((c) => this.calendarFeeds.map((cf) => cf.name).includes(c.name))
         .flatMap((cf) =>
           cf.events.map((event) => {
@@ -184,6 +232,34 @@ export default {
             };
           }),
         );
+      if (this.internalCalendarType === "month" || this.showAllAllDayEvents) {
+        return events;
+      }
+
+      let dateFullEventCount = {};
+      this.clearDaysWithHiddenEvents();
+
+      return events.filter((event) => {
+        if (!event.allDay) {
+          return true;
+        }
+        const start = event.startDateTime;
+        dateFullEventCount[start] = (dateFullEventCount[start] || 0) + 1;
+        const show = dateFullEventCount[start] <= this.maxAllDayEvents;
+        if (!show) {
+          const dateInterval = Interval.fromDateTimes(
+            start,
+            event.endDateTime.endOf("day"),
+          )
+            .splitBy({ day: 1 })
+            .map((date) => date.start.toISODate());
+          for (const date of dateInterval) {
+            this.daysWithHiddenEvents[date] =
+              (this.daysWithHiddenEvents[date] || 0) + 1;
+          }
+        }
+        return show;
+      });
     },
     paramsForSend() {
       if (this.params !== null) {
@@ -251,6 +327,9 @@ export default {
 
       return this.personByIdOrMe.preferences.daysOfWeek;
     },
+    showAllAllDayEventsButtonIcon() {
+      return this.showAllAllDayEvents ? "mdi-chevron-up" : "mdi-chevron-down";
+    },
   },
   watch: {
     params(newParams) {
@@ -526,6 +605,9 @@ export default {
       // TODO: is this destroyed when unloading?
       setInterval(() => this.cal.updateTimes(), 60 * 1000);
     },
+    clearDaysWithHiddenEvents() {
+      this.daysWithHiddenEvents = {};
+    },
   },
   mounted() {
     this.ready = true;
diff --git a/aleksis/core/frontend/css/global.scss b/aleksis/core/frontend/css/global.scss
index 6bbdf6e47..135927a85 100644
--- a/aleksis/core/frontend/css/global.scss
+++ b/aleksis/core/frontend/css/global.scss
@@ -39,3 +39,8 @@ h6,
 .full-width {
   width: 100%;
 }
+
+.v-calendar-daily_head-day {
+  display: flex;
+  flex-direction: column;
+}
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index aa81486df..e68e9a294 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -134,7 +134,8 @@
     "my_calendars": "My Calendars",
     "select": "Select calendars",
     "today": "Today",
-    "week": "Week"
+    "week": "Week",
+    "more_events": "{n} more"
   },
   "celery_progress": {
     "error_message": "The operation couldn't be finished successfully.",
-- 
GitLab