From 12963317963f6aa00d9339ee5516e5f42c3f4c79 Mon Sep 17 00:00:00 2001
From: Julian Krauser <jkrauser209@gmail.com>
Date: Mon, 14 Apr 2025 09:12:00 +0200
Subject: [PATCH 1/2] extend calendar by list

---
 package-lock.json                          |  10 ++
 package.json                               |   1 +
 src/components/CustomCalendar.vue          | 170 +++++++++++++++++++++
 src/main.css                               |  28 +---
 src/views/admin/club/calendar/Calendar.vue | 103 ++-----------
 src/views/public/calendar/Calendar.vue     |  38 +----
 6 files changed, 198 insertions(+), 152 deletions(-)
 create mode 100644 src/components/CustomCalendar.vue

diff --git a/package-lock.json b/package-lock.json
index f1e26a7..a08af4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
         "@fullcalendar/core": "^6.1.15",
         "@fullcalendar/daygrid": "^6.1.15",
         "@fullcalendar/interaction": "^6.1.15",
+        "@fullcalendar/list": "^6.1.17",
         "@fullcalendar/timegrid": "^6.1.15",
         "@fullcalendar/vue3": "^6.1.15",
         "@headlessui/vue": "^1.7.23",
@@ -2379,6 +2380,15 @@
         "@fullcalendar/core": "~6.1.17"
       }
     },
+    "node_modules/@fullcalendar/list": {
+      "version": "6.1.17",
+      "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.17.tgz",
+      "integrity": "sha512-fkyK49F9IxwlGUBVhJGsFpd/LTi/vRVERLIAe1HmBaGkjwpxnynm8TMLb9mZip97wvDk3CmZWduMe6PxscAlow==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@fullcalendar/core": "~6.1.17"
+      }
+    },
     "node_modules/@fullcalendar/timegrid": {
       "version": "6.1.17",
       "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.17.tgz",
diff --git a/package.json b/package.json
index 4ffd7d2..aa87b2c 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
     "@fullcalendar/core": "^6.1.15",
     "@fullcalendar/daygrid": "^6.1.15",
     "@fullcalendar/interaction": "^6.1.15",
+    "@fullcalendar/list": "^6.1.17",
     "@fullcalendar/timegrid": "^6.1.15",
     "@fullcalendar/vue3": "^6.1.15",
     "@headlessui/vue": "^1.7.23",
diff --git a/src/components/CustomCalendar.vue b/src/components/CustomCalendar.vue
new file mode 100644
index 0000000..e31d2d6
--- /dev/null
+++ b/src/components/CustomCalendar.vue
@@ -0,0 +1,170 @@
+<template>
+  <div class="flex flex-col w-full h-full gap-2 justify-between overflow-hidden">
+    <div class="flex flex-row gap-2 max-xl:flex-wrap justify-between max-sm:justify-center">
+      <div class="flex flex-row max-xl:order-2">
+        <button
+          :primary="view == 'dayGridMonth'"
+          :primary-outline="view != 'dayGridMonth'"
+          class="rounded-r-none!"
+          @click="setView('dayGridMonth')"
+        >
+          Monat
+        </button>
+        <button
+          :primary="view == 'timeGridWeek'"
+          :primary-outline="view != 'timeGridWeek'"
+          class="rounded-none! border-x-0!"
+          @click="setView('timeGridWeek')"
+        >
+          Woche
+        </button>
+        <button
+          :primary="view == 'listMonth'"
+          :primary-outline="view != 'listMonth'"
+          class="rounded-l-none!"
+          @click="setView('listMonth')"
+        >
+          Liste
+        </button>
+      </div>
+      <p class="text-3xl w-full max-xl:order-1 text-center">{{ currentTitle }}</p>
+      <div class="flex flex-row max-xl:order-3">
+        <button primary-outline class="rounded-r-none!" @click="navigateView('prev')">
+          <ChevronLeftIcon />
+        </button>
+        <button
+          :primary="containsToday"
+          :primary-outline="!containsToday"
+          class="rounded-none! border-x-0!"
+          @click="
+            calendarApi?.today();
+            containsToday = true;
+          "
+        >
+          heute
+        </button>
+        <button primary-outline class="rounded-l-none!" @click="navigateView('next')">
+          <ChevronRightIcon />
+        </button>
+      </div>
+    </div>
+    <FullCalendar ref="fullCalendar" :options="calendarOptions" class="max-h-full h-full" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { defineComponent, type PropType } from "vue";
+import FullCalendar from "@fullcalendar/vue3";
+import deLocale from "@fullcalendar/core/locales/de";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import listPlugin from "@fullcalendar/list";
+import interactionPlugin from "@fullcalendar/interaction";
+import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/24/outline";
+import type { CalendarOptions } from "@fullcalendar/core/index.js";
+</script>
+
+<script lang="ts">
+export default defineComponent({
+  props: {
+    items: {
+      type: Array as PropType<
+        {
+          id: string;
+          title: string;
+          start: string;
+          end: string;
+          backgroundColor: string;
+        }[]
+      >,
+      default: [],
+    },
+    allowInteraction: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  emits: {
+    dateSelect: ({ start, end, allDay }: { start: string; end: string; allDay: boolean }) => {
+      return typeof start == "string" && typeof end == "string" && typeof allDay === "boolean";
+    },
+    eventSelect: (id: string) => {
+      return typeof id == "string";
+    },
+  },
+  data() {
+    return {
+      view: "dayGridMonth" as "dayGridMonth" | "timeGridWeek" | "listMonth",
+      calendarApi: null as null | typeof FullCalendar,
+      currentTitle: "" as string,
+      containsToday: false as boolean,
+    };
+  },
+  computed: {
+    calendarOptions() {
+      return {
+        timeZone: "local",
+        locale: deLocale,
+        plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
+        initialView: "dayGridMonth",
+        eventDisplay: "block",
+        headerToolbar: false,
+        weekends: true,
+        editable: this.allowInteraction,
+        selectable: this.allowInteraction,
+        selectMirror: false,
+        dayMaxEvents: true,
+        weekNumbers: true,
+        displayEventTime: true,
+        nowIndicator: true,
+        weekText: "KW",
+        allDaySlot: false,
+        events: this.items,
+        select: this.select,
+        eventClick: this.eventClick,
+      } as CalendarOptions;
+    },
+  },
+  mounted() {
+    this.calendarApi = (this.$refs.fullCalendar as typeof FullCalendar).getApi();
+    this.setTitle();
+    this.setContainsToday();
+  },
+  methods: {
+    setTitle() {
+      this.currentTitle = this.calendarApi?.view.title ?? "";
+    },
+    setView(view: "dayGridMonth" | "timeGridWeek" | "listMonth") {
+      this.calendarApi?.changeView(view);
+      this.view = view;
+      this.setTitle();
+      this.setContainsToday();
+    },
+    navigateView(change: "prev" | "next") {
+      if (change == "prev") {
+        this.calendarApi?.prev();
+      } else {
+        this.calendarApi?.next();
+      }
+      this.setTitle();
+      this.setContainsToday();
+    },
+    setContainsToday() {
+      const start = this.calendarApi?.view.currentStart;
+      const end = this.calendarApi?.view.currentEnd;
+      const today = new Date();
+      this.containsToday = today >= start && today < end;
+    },
+    select(e: any) {
+      this.$emit("dateSelect", {
+        start: e?.startStr ?? new Date().toISOString(),
+        end: e?.endStr ?? new Date().toISOString(),
+        allDay: e?.allDay ?? false,
+      });
+    },
+    eventClick(e: any) {
+      this.$emit("eventSelect", e.event.id);
+    },
+  },
+});
+</script>
diff --git a/src/main.css b/src/main.css
index 9816a0e..e618f2f 100644
--- a/src/main.css
+++ b/src/main.css
@@ -72,7 +72,7 @@ a[button] {
 
 button[primary]:not([primary="false"]),
 a[button][primary]:not([primary="false"]) {
-  @apply border border-transparent text-white bg-primary hover:bg-primary;
+  @apply border-2 border-transparent text-white bg-primary hover:bg-primary;
 }
 
 button[primary-outline]:not([primary-outline="false"]),
@@ -131,29 +131,3 @@ summary > svg {
 summary::-webkit-details-marker {
   display: none;
 }
-
-.fc-button-primary {
-  @apply bg-primary! border-primary! outline-hidden! ring-0! hover:bg-red-700! hover:border-red-700! h-10 text-center;
-}
-.fc-button-active {
-  @apply bg-red-500! border-red-500!;
-}
-.fc-toolbar {
-  @apply flex-wrap;
-}
-
-/* For screens between 850px and 768px */
-@media (max-width: 850px) and (min-width: 768px) {
-  .fc-header-toolbar.fc-toolbar.fc-toolbar-ltr > .fc-toolbar-chunk:nth-child(2) {
-    @apply order-1!;
-  }
-  /* Your styles for this range */
-}
-
-/* For screens between 525px and 0px */
-@media (max-width: 525px) and (min-width: 0px) {
-  /* Your styles for this range */
-  .fc-header-toolbar.fc-toolbar.fc-toolbar-ltr > .fc-toolbar-chunk:nth-child(2) {
-    @apply order-1!;
-  }
-}
diff --git a/src/views/admin/club/calendar/Calendar.vue b/src/views/admin/club/calendar/Calendar.vue
index d45bd41..4535be8 100644
--- a/src/views/admin/club/calendar/Calendar.vue
+++ b/src/views/admin/club/calendar/Calendar.vue
@@ -4,14 +4,17 @@
       <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
         <h1 class="font-bold text-xl h-8">Kalender</h1>
         <div class="flex flex-row gap-2">
-          <PlusIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="select" />
+          <PlusIcon
+            class="text-gray-500 h-5 w-5 cursor-pointer"
+            @click="select({ start: '', end: '', allDay: false })"
+          />
           <LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
         </div>
       </div>
     </template>
     <template #diffMain>
       <div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden">
-        <FullCalendar :options="calendarOptions" class="max-h-full h-full" />
+        <CustomCalendar :items="formattedItems" @date-select="select" @event-select="eventClick" />
       </div>
     </template>
   </MainTemplate>
@@ -22,14 +25,10 @@ import { defineComponent, markRaw, defineAsyncComponent } from "vue";
 import { mapActions, mapState } from "pinia";
 import { useModalStore } from "@/stores/modal";
 import MainTemplate from "@/templates/Main.vue";
-import FullCalendar from "@fullcalendar/vue3";
-import deLocale from "@fullcalendar/core/locales/de";
-import dayGridPlugin from "@fullcalendar/daygrid";
-import timeGridPlugin from "@fullcalendar/timegrid";
-import interactionPlugin from "@fullcalendar/interaction";
 import { useCalendarStore } from "@/stores/admin/club/calendar";
 import { useAbilityStore } from "@/stores/ability";
 import { LinkIcon, PlusIcon } from "@heroicons/vue/24/outline";
+import CustomCalendar from "@/components/CustomCalendar.vue";
 </script>
 
 <script lang="ts">
@@ -40,33 +39,6 @@ export default defineComponent({
   computed: {
     ...mapState(useCalendarStore, ["formattedItems"]),
     ...mapState(useAbilityStore, ["can"]),
-    calendarOptions() {
-      return {
-        timeZone: "local",
-        locale: deLocale,
-        plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
-        initialView: "dayGridMonth",
-        headerToolbar: {
-          left: "dayGridMonth,timeGridWeek",
-          center: "title",
-          right: "prev,today,next",
-        },
-        eventDisplay: "block",
-        weekends: true,
-        editable: true,
-        selectable: true,
-        selectMirror: false,
-        dayMaxEvents: true,
-        weekNumbers: true,
-        displayEventTime: true,
-        nowIndicator: true,
-        weekText: "KW",
-        allDaySlot: false,
-        events: this.formattedItems,
-        select: this.select,
-        eventClick: this.eventClick,
-      };
-    },
   },
   mounted() {
     this.fetchCalendars();
@@ -74,22 +46,22 @@ export default defineComponent({
   methods: {
     ...mapActions(useModalStore, ["openModal"]),
     ...mapActions(useCalendarStore, ["fetchCalendars"]),
-    select(e: any) {
+    select({ start, end, allDay }: { start: string; end: string; allDay: boolean }) {
       if (!this.can("create", "club", "calendar")) return;
       this.openModal(
         markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/CreateCalendarModal.vue"))),
         {
-          start: e?.startStr ?? new Date().toISOString(),
-          end: e?.endStr ?? new Date().toISOString(),
-          allDay: e?.allDay ?? false,
+          start,
+          end,
+          allDay,
         }
       );
     },
-    eventClick(e: any) {
+    eventClick(id: string) {
       if (!this.can("update", "club", "calendar")) return;
       this.openModal(
         markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/UpdateCalendarModal.vue"))),
-        e.event.id
+        id
       );
     },
     openLinkModal(e: any) {
@@ -99,55 +71,4 @@ export default defineComponent({
     },
   },
 });
-
-/**
-locale: deLocale,
-      events: this.absencesList.map((x) => ({
-        id: x.absenceId,
-        start: x.startDate,
-        end: x.endDate,
-        allday: true,
-        backgroundColor: this.getColorForAbsenceType(x.absenceType),
-        borderColor: '#ffffff',
-        title: this.getAbsenceType(x.absenceType) + ' ' + x.fullName,
-      })),
-      plugins: [
-        interactionPlugin,
-        dayGridPlugin,
-        timeGridPlugin,
-        listPlugin,
-        multiMonthPlugin,
-      ],
-      initialView: 'dayGridMonth',
-      eventDisplay: 'block',
-      weekends: false,
-      editable: true,
-      selectable: true,
-      selectMirror: true,
-      dayMaxEvents: true,
-      weekNumbers: true,
-      displayEventTime: false,
-      weekText: 'KW',
-      validRange: { start: '2023-01-01', end: '' },
-      headerToolbar: {
-        left: 'today prev,next',
-        center: 'title',
-        right: 'listMonth,dayGridMonth,multiMonthYear,customview',
-      },
-      views: {
-        customview: {
-          type: 'multiMonth',
-          multiMonthMaxColumns: 1,
-          duration: { month: 12 },
-          buttonText: 'grid',
-        },
-      },
-      dateClick: this.handleDateSelect.bind(this),
-      datesSet: this.handleMonthChange.bind(this),
-      select: this.handleDateSelect.bind(this),
-      eventClick: this.handleEventClick.bind(this),
-      eventsSet: this.handleEvents.bind(this),
-    }; 
-
- */
 </script>
diff --git a/src/views/public/calendar/Calendar.vue b/src/views/public/calendar/Calendar.vue
index 956a34d..f4de6da 100644
--- a/src/views/public/calendar/Calendar.vue
+++ b/src/views/public/calendar/Calendar.vue
@@ -12,7 +12,7 @@
     </template>
     <template #diffMain>
       <div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden">
-        <FullCalendar :options="calendarOptions" class="max-h-full h-full" />
+        <CustomCalendar :items="formattedItems" :allow-interaction="false" @event-select="eventClick" />
       </div>
     </template>
   </MainTemplate>
@@ -23,14 +23,10 @@ import { defineComponent, markRaw, defineAsyncComponent } from "vue";
 import { mapActions, mapState } from "pinia";
 import { useModalStore } from "@/stores/modal";
 import MainTemplate from "@/templates/Main.vue";
-import FullCalendar from "@fullcalendar/vue3";
-import deLocale from "@fullcalendar/core/locales/de";
-import dayGridPlugin from "@fullcalendar/daygrid";
-import timeGridPlugin from "@fullcalendar/timegrid";
-import interactionPlugin from "@fullcalendar/interaction";
 import { InformationCircleIcon, LinkIcon } from "@heroicons/vue/24/outline";
 import type { CalendarViewModel } from "@/viewmodels/admin/club/calendar.models";
 import { RouterLink } from "vue-router";
+import CustomCalendar from "@/components/CustomCalendar.vue";
 </script>
 
 <script lang="ts">
@@ -50,32 +46,6 @@ export default defineComponent({
         backgroundColor: c.type.color,
       }));
     },
-    calendarOptions() {
-      return {
-        timeZone: "local",
-        locale: deLocale,
-        plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
-        initialView: "dayGridMonth",
-        headerToolbar: {
-          left: "dayGridMonth,timeGridWeek",
-          center: "title",
-          right: "prev,today,next",
-        },
-        eventDisplay: "block",
-        weekends: true,
-        editable: false,
-        selectable: false,
-        selectMirror: false,
-        dayMaxEvents: true,
-        weekNumbers: true,
-        displayEventTime: true,
-        nowIndicator: true,
-        weekText: "KW",
-        allDaySlot: false,
-        events: this.formattedItems,
-        eventClick: this.eventClick,
-      };
-    },
   },
   mounted() {
     this.fetchCalendars();
@@ -93,10 +63,10 @@ export default defineComponent({
     openLinkModal(e: any) {
       this.openModal(markRaw(defineAsyncComponent(() => import("@/components/public/calendar/CalendarLinkModal.vue"))));
     },
-    eventClick(e: any) {
+    eventClick(id: string) {
       this.openModal(
         markRaw(defineAsyncComponent(() => import("@/components/public/calendar/ShowCalendarEntryModal.vue"))),
-        this.calendars.find((c) => c.id == e.event.id)
+        this.calendars.find((c) => c.id == id)
       );
     },
   },

From 6d453255433ba532868dbb2739fa24e34decd18b Mon Sep 17 00:00:00 2001
From: Julian Krauser <jkrauser209@gmail.com>
Date: Mon, 14 Apr 2025 09:17:32 +0200
Subject: [PATCH 2/2] add styling option for wide calendar view

---
 src/components/CustomCalendar.vue      | 17 +++++++++++++----
 src/views/public/calendar/Calendar.vue |  7 ++++++-
 2 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/src/components/CustomCalendar.vue b/src/components/CustomCalendar.vue
index e31d2d6..edafd21 100644
--- a/src/components/CustomCalendar.vue
+++ b/src/components/CustomCalendar.vue
@@ -1,7 +1,10 @@
 <template>
   <div class="flex flex-col w-full h-full gap-2 justify-between overflow-hidden">
-    <div class="flex flex-row gap-2 max-xl:flex-wrap justify-between max-sm:justify-center">
-      <div class="flex flex-row max-xl:order-2">
+    <div
+      class="flex flex-row gap-2 justify-between max-sm:justify-center"
+      :class="smallStyling ? 'max-lg:flex-wrap' : 'max-xl:flex-wrap'"
+    >
+      <div class="flex flex-row" :class="smallStyling ? 'max-lg:order-2' : 'max-xl:order-2'">
         <button
           :primary="view == 'dayGridMonth'"
           :primary-outline="view != 'dayGridMonth'"
@@ -27,8 +30,10 @@
           Liste
         </button>
       </div>
-      <p class="text-3xl w-full max-xl:order-1 text-center">{{ currentTitle }}</p>
-      <div class="flex flex-row max-xl:order-3">
+      <p class="text-3xl w-full text-center" :class="smallStyling ? 'max-lg:order-1' : 'max-xl:order-1'">
+        {{ currentTitle }}
+      </p>
+      <div class="flex flex-row" :class="smallStyling ? 'max-lg:order-3' : 'max-xl:order-3'">
         <button primary-outline class="rounded-r-none!" @click="navigateView('prev')">
           <ChevronLeftIcon />
         </button>
@@ -83,6 +88,10 @@ export default defineComponent({
       type: Boolean,
       default: true,
     },
+    smallStyling: {
+      type: Boolean,
+      default: false,
+    },
   },
   emits: {
     dateSelect: ({ start, end, allDay }: { start: string; end: string; allDay: boolean }) => {
diff --git a/src/views/public/calendar/Calendar.vue b/src/views/public/calendar/Calendar.vue
index f4de6da..8c68586 100644
--- a/src/views/public/calendar/Calendar.vue
+++ b/src/views/public/calendar/Calendar.vue
@@ -12,7 +12,12 @@
     </template>
     <template #diffMain>
       <div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden">
-        <CustomCalendar :items="formattedItems" :allow-interaction="false" @event-select="eventClick" />
+        <CustomCalendar
+          :items="formattedItems"
+          :allow-interaction="false"
+          :small-styling="true"
+          @event-select="eventClick"
+        />
       </div>
     </template>
   </MainTemplate>