calendar link and typed link

This commit is contained in:
Julian Krauser 2024-12-05 16:37:10 +01:00
parent e9a3e0c872
commit bf3e0cd245
10 changed files with 236 additions and 5 deletions

View file

@ -30,11 +30,15 @@ services:
container_name: ff_member_administration_ui container_name: ff_member_administration_ui
restart: unless-stopped restart: unless-stopped
#environment:
# - SERVER_ADRESS=<backend_host> # wichtig: ohne https:// bzw http://
#volumes: #volumes:
# - <volume|local path>/favicon.png:/app/public/favicon.png # - <volume|local path>/favicon.png:/app/public/favicon.png
# - <volume|local path>/favicon.png:/app/public/FFW-Logo.svg # - <volume|local path>/favicon.png:/app/public/FFW-Logo.svg
``` ```
Wenn keine Server-Adresse angegeben wird, wird versucht das Backend unter der URL des Frontends zu erreichen. Dazu muss das Backend auf der gleichen URL wie das Frontend laufen. Zur Unterscheidung von Frontend und Backend bei gleicher URL müssen alle Anfragen mit dem PathPrefix `/api` an das Backend weitergeleitet werden.
Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten: Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
```sh ```sh

View file

@ -0,0 +1,138 @@
<template>
<div class="relative w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Kalenderlink</p>
</div>
<br />
<div class="flex flex-col gap-2">
<div>
<Listbox v-model="selectedTypes" name="type" multiple>
<ListboxLabel>Typen zur Anzeige auswählen</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
selectedTypes.length != 0
? selectedTypes?.map((t) => t.type).join(", ")
: "Standard-Typen werden ausgeliefert"
}}
</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
</li>
</ListboxOption>
<ListboxOption
v-slot="{ active, selected }"
v-for="type in calendarTypes"
:key="type.id"
:value="type"
as="template"
>
<li
:class="[
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4',
]"
>
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">
{{ type.type }}
<small v-if="type.passphrase">(passwortgeschützt)</small>
<small v-if="type.nscdr">(standard-Auslieferung)</small>
</span>
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<p class="flex flex-row text-sm">
<InformationCircleIcon class="text-gray-500 h-5 w-5" /> Wenn kein Typ ausgewählt ist, werden die Standard-Typen
zur Verfügung gestellt: <br />
-> {{ defaultTypes }}
</p>
<br />
<TextCopy :copyText="generatedLink" />
<br />
</div>
<RouterLink
:to="{ name: 'public-calendar' }"
title="Zur öffentlichen Kalender Anzeige"
class="absolute top-3 right-3"
target="_blank"
>
<CalendarDaysIcon class="text-gray-500 h-5 w-5" />
</RouterLink>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">schließen</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { RouterLink } from "vue-router";
import { useModalStore } from "@/stores/modal";
import { useCalendarTypeStore } from "@/stores/admin/calendarType";
import type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import TextCopy from "@/components/TextCopy.vue";
import { CalendarDaysIcon, InformationCircleIcon } from "@heroicons/vue/24/outline";
import { host } from "@/serverCom";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
selectedTypes: [] as Array<CalendarTypeViewModel>,
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useCalendarTypeStore, ["calendarTypes"]),
defaultTypes() {
return this.calendarTypes
.filter((t) => t.nscdr)
.map((t) => t.type)
.join(", ");
},
generatedLink() {
let extend = this.selectedTypes.map((t) => [t.type, t.passphrase].filter((at) => at).join(":"));
return `webcal://${host || window.location.host}/api/public/calendar${extend.length == 0 ? "" : "?types=" + extend.join("&types=")}`;
},
},
mounted() {
this.fetchCalendarTypes();
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
},
});
</script>

View file

@ -7,6 +7,7 @@
</div> </div>
<p>{{ calendarType.type }}</p> <p>{{ calendarType.type }}</p>
<small v-if="calendarType.passphrase">(passwortgeschützt)</small> <small v-if="calendarType.passphrase">(passwortgeschützt)</small>
<small v-if="calendarType.nscdr">(standard-Auslieferung)</small>
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
<RouterLink <RouterLink

View file

@ -0,0 +1,36 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Kalenderlink</p>
</div>
<br />
<TextCopy :copyText="generatedLink" />
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">schließen</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import TextCopy from "@/components/TextCopy.vue";
import { host } from "@/serverCom";
</script>
<script lang="ts">
export default defineComponent({
computed: {
generatedLink() {
return `webcal://${host || window.location.host}/api/public/calendar`;
},
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
},
});
</script>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-full flex flex-row gap-4"> <div class="w-full h-full flex flex-row gap-4">
<div class="max-w-full flex grow gap-4 md:flex-row flex-col"> <div class="max-w-full flex grow gap-4 flex-col">
<slot name="main"></slot> <slot name="main"></slot>
</div> </div>
</div> </div>

View file

@ -540,6 +540,11 @@ const router = createRouter({
name: "public-calendar", name: "public-calendar",
component: () => import("@/views/public/calendar/Calendar.vue"), component: () => import("@/views/public/calendar/Calendar.vue"),
}, },
{
path: "calendar-explain",
name: "public-calendar-explain",
component: () => import("@/views/public/calendar/CalendarExplain.vue"),
},
], ],
}, },
{ {

View file

@ -4,8 +4,11 @@ import router from "./router";
let devMode = process.env.NODE_ENV === "development"; let devMode = process.env.NODE_ENV === "development";
let host = devMode ? "localhost:5000" : process.env.SERVER_ADDRESS || "";
let url = devMode ? "http://" + host : (host ? "https://" : "") + host;
const http = axios.create({ const http = axios.create({
baseURL: (devMode ? "http://localhost:5000" : "") + "/api", baseURL: url + "/api",
headers: { headers: {
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
Pragma: "no-cache", Pragma: "no-cache",
@ -81,4 +84,4 @@ export async function refreshToken(): Promise<void> {
}); });
} }
export { http }; export { http, host };

View file

@ -3,6 +3,7 @@
<template #topBar> <template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <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> <h1 class="font-bold text-xl h-8">Kalender</h1>
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
</div> </div>
</template> </template>
<template #diffMain> <template #diffMain>
@ -25,6 +26,7 @@ import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import { useCalendarStore } from "@/stores/admin/calendar"; import { useCalendarStore } from "@/stores/admin/calendar";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { LinkIcon } from "@heroicons/vue/24/outline";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -87,6 +89,11 @@ export default defineComponent({
e.event.id e.event.id
); );
}, },
openLinkModal(e: any) {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/CalendarLinkModal.vue")))
);
},
}, },
}); });

View file

@ -1,8 +1,13 @@
<template> <template>
<MainTemplate :showBack="false"> <MainTemplate :showBack="false">
<template #topBar> <template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <div class="flex flex-row items-center gap-4 pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Kalender</h1> <h1 class="font-bold text-xl h-8">Kalender</h1>
<div class="grow"></div>
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
<RouterLink :to="{ name: 'public-calendar-explain' }">
<InformationCircleIcon class="text-gray-500 h-5 w-5 cursor-pointer" />
</RouterLink>
</div> </div>
</template> </template>
<template #diffMain> <template #diffMain>
@ -14,14 +19,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineComponent, markRaw, defineAsyncComponent } from "vue";
import { mapActions, mapState } from "pinia";
import { useModalStore } from "@/stores/modal";
import MainTemplate from "@/templates/Main.vue"; import MainTemplate from "@/templates/Main.vue";
import FullCalendar from "@fullcalendar/vue3"; import FullCalendar from "@fullcalendar/vue3";
import deLocale from "@fullcalendar/core/locales/de"; import deLocale from "@fullcalendar/core/locales/de";
import dayGridPlugin from "@fullcalendar/daygrid"; import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import { InformationCircleIcon, LinkIcon } from "@heroicons/vue/24/outline";
import type { CalendarViewModel } from "@/viewmodels/admin/calendar.models"; import type { CalendarViewModel } from "@/viewmodels/admin/calendar.models";
import { RouterLink } from "vue-router";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -71,6 +80,7 @@ export default defineComponent({
this.fetchCalendars(); this.fetchCalendars();
}, },
methods: { methods: {
...mapActions(useModalStore, ["openModal"]),
fetchCalendars() { fetchCalendars() {
this.$http this.$http
.get("/public/calendar?output=json") .get("/public/calendar?output=json")
@ -79,6 +89,9 @@ export default defineComponent({
}) })
.catch((err) => {}); .catch((err) => {});
}, },
openLinkModal(e: any) {
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/public/calendar/CalendarLinkModal.vue"))));
},
}, },
}); });
</script> </script>

View file

@ -0,0 +1,24 @@
<template>
<MainTemplate :showBack="false">
<template #headerInsert>
<RouterLink
:to="{
name: 'public-calendar',
}"
class="mid:hidden text-primary"
>
zum Kalender
</RouterLink>
</template>
<template #topBar>
<div class="flex flex-row items-center gap-2 pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Kalender-Erklärung</h1>
</div>
</template>
<template #main> Kalender-Erklärung zur Nutzung </template>
</MainTemplate>
</template>
<script setup lang="ts">
import MainTemplate from "@/templates/Main.vue";
</script>