#18-public-calendar #19
19 changed files with 428 additions and 9 deletions
|
@ -30,11 +30,15 @@ services:
|
|||
container_name: ff_member_administration_ui
|
||||
restart: unless-stopped
|
||||
|
||||
#environment:
|
||||
# - SERVER_ADRESS=<backend_host> # wichtig: ohne https:// bzw http://
|
||||
#volumes:
|
||||
# - <volume|local path>/favicon.png:/app/public/favicon.png
|
||||
# - <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:
|
||||
|
||||
```sh
|
||||
|
|
BIN
public/calendar.webp
Normal file
BIN
public/calendar.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 312 KiB |
|
@ -5,13 +5,17 @@
|
|||
>
|
||||
<div class="w-full flex flex-row gap-2 h-full align-middle">
|
||||
<TopLevelLink
|
||||
v-if="routeName.includes('admin-')"
|
||||
v-if="routeName == 'admin' || routeName.includes('admin-')"
|
||||
v-for="item in topLevel"
|
||||
:key="item.key"
|
||||
:link="item"
|
||||
:disableSubLink="true"
|
||||
/>
|
||||
<TopLevelLink v-else :link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }" :disableSubLink="true" />
|
||||
<TopLevelLink
|
||||
v-else-if="routeName == 'account' || routeName.includes('account-')"
|
||||
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
||||
:disableSubLink="true"
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
|
|
@ -6,9 +6,14 @@
|
|||
</RouterLink>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle">
|
||||
<TopLevelLink v-if="routeName.includes('admin-')" v-for="item in topLevel" :key="item.key" :link="item" />
|
||||
<TopLevelLink
|
||||
v-else-if="routeName.includes('account-')"
|
||||
v-if="routeName == 'admin' || routeName.includes('admin-')"
|
||||
v-for="item in topLevel"
|
||||
:key="item.key"
|
||||
:link="item"
|
||||
/>
|
||||
<TopLevelLink
|
||||
v-else-if="routeName == 'account' || routeName.includes('account-')"
|
||||
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
||||
:disable-sub-link="true"
|
||||
/>
|
||||
|
|
138
src/components/admin/club/calendar/CalendarLinkModal.vue
Normal file
138
src/components/admin/club/calendar/CalendarLinkModal.vue
Normal 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>
|
|
@ -6,6 +6,8 @@
|
|||
<EyeIcon v-if="calendarType.nscdr" class="w-5 h-5" />
|
||||
</div>
|
||||
<p>{{ calendarType.type }}</p>
|
||||
<small v-if="calendarType.passphrase">(passwortgeschützt)</small>
|
||||
<small v-if="calendarType.nscdr">(standard-Auslieferung)</small>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<RouterLink
|
||||
|
|
|
@ -14,9 +14,13 @@
|
|||
<label for="color">Farbe</label>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input type="checkbox" id="nscdr" />
|
||||
<input type="checkbox" id="nscdr" v-model="nscdr" />
|
||||
<label for="nscdr">Standard Kalender Auslieferung (optional)</label>
|
||||
</div>
|
||||
<div v-if="!nscdr">
|
||||
<label for="passphrase">Passphrase (optional)</label>
|
||||
<input type="text" id="passphrase" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||
|
@ -59,6 +63,7 @@ export default defineComponent({
|
|||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
nscdr: false as boolean,
|
||||
};
|
||||
},
|
||||
beforeUnmount() {
|
||||
|
@ -75,6 +80,7 @@ export default defineComponent({
|
|||
type: formData.type.value,
|
||||
color: formData.color.value,
|
||||
nscdr: formData.nscdr.checked,
|
||||
passphrase: formData.passphrase.value,
|
||||
};
|
||||
this.createCalendarType(createCalendarType)
|
||||
.then(() => {
|
||||
|
|
36
src/components/public/calendar/CalendarLinkModal.vue
Normal file
36
src/components/public/calendar/CalendarLinkModal.vue
Normal 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>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -81,7 +81,7 @@ const router = createRouter({
|
|||
{
|
||||
path: "",
|
||||
name: "admin-default",
|
||||
component: () => import("@/views/RouterView.vue"),
|
||||
component: () => import("@/views/admin/ViewSelect.vue"),
|
||||
},
|
||||
{
|
||||
path: "club",
|
||||
|
@ -525,6 +525,28 @@ const router = createRouter({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/public",
|
||||
name: "public",
|
||||
component: () => import("@/views/public/View.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "public-default",
|
||||
component: () => import("@/views/notFound.vue"),
|
||||
},
|
||||
{
|
||||
path: "calendar",
|
||||
name: "public-calendar",
|
||||
component: () => import("@/views/public/calendar/Calendar.vue"),
|
||||
},
|
||||
{
|
||||
path: "calendar-explain",
|
||||
name: "public-calendar-explain",
|
||||
component: () => import("@/views/public/calendar/CalendarExplain.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/nopermissions",
|
||||
name: "nopermissions",
|
||||
|
|
|
@ -4,8 +4,11 @@ import router from "./router";
|
|||
|
||||
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({
|
||||
baseURL: (devMode ? "http://localhost:5000" : "") + "/api",
|
||||
baseURL: url + "/api",
|
||||
headers: {
|
||||
"Cache-Control": "no-cache",
|
||||
Pragma: "no-cache",
|
||||
|
@ -81,4 +84,4 @@ export async function refreshToken(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export { http };
|
||||
export { http, host };
|
||||
|
|
|
@ -36,6 +36,7 @@ export const useCalendarTypeStore = defineStore("calendarType", {
|
|||
type: calendarType.type,
|
||||
nscdr: calendarType.nscdr,
|
||||
color: calendarType.color,
|
||||
passphrase: calendarType.passphrase,
|
||||
});
|
||||
this.fetchCalendarTypes();
|
||||
return result;
|
||||
|
@ -45,6 +46,7 @@ export const useCalendarTypeStore = defineStore("calendarType", {
|
|||
type: calendarType.type,
|
||||
nscdr: calendarType.nscdr,
|
||||
color: calendarType.color,
|
||||
passphrase: calendarType.passphrase,
|
||||
});
|
||||
this.fetchCalendarTypes();
|
||||
return result;
|
||||
|
|
|
@ -3,12 +3,14 @@ export interface CalendarTypeViewModel {
|
|||
type: string;
|
||||
nscdr: boolean;
|
||||
color: string;
|
||||
passphrase: string | null;
|
||||
}
|
||||
|
||||
export interface CreateCalendarTypeViewModel {
|
||||
type: string;
|
||||
nscdr: boolean;
|
||||
color: string;
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCalendarTypeViewModel {
|
||||
|
@ -16,4 +18,5 @@ export interface UpdateCalendarTypeViewModel {
|
|||
type: string;
|
||||
nscdr: boolean;
|
||||
color: string;
|
||||
passphrase?: string;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<template #topBar>
|
||||
<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>
|
||||
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
|
@ -25,6 +26,7 @@ import timeGridPlugin from "@fullcalendar/timegrid";
|
|||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import { useCalendarStore } from "@/stores/admin/calendar";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { LinkIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -87,6 +89,11 @@ export default defineComponent({
|
|||
e.event.id
|
||||
);
|
||||
},
|
||||
openLinkModal(e: any) {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/CalendarLinkModal.vue")))
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -91,6 +91,12 @@ export default defineComponent({
|
|||
},
|
||||
// this.syncState is undefined, so it will never work
|
||||
// beforeRouteLeave(to, from, next) {
|
||||
// const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
|
||||
// if (answer) {
|
||||
// next()
|
||||
// } else {
|
||||
// next(false)
|
||||
// }
|
||||
// if (this.syncState != "synced") {
|
||||
// this.executeSyncAll = Date.now();
|
||||
// this.wantToClose = true;
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
<input type="checkbox" id="nscdr" v-model="calendarType.nscdr" />
|
||||
<label for="nscdr">Standard Kalender Auslieferung (optional)</label>
|
||||
</div>
|
||||
<div v-if="!calendarType.nscdr">
|
||||
<label for="passphrase">Passphrase (optional)</label>
|
||||
<input type="text" id="passphrase" v-model="calendarType.passphrase" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<button primary-outline type="reset" class="!w-fit" :disabled="canSaveOrReset" @click="resetForm">
|
||||
|
@ -111,6 +115,7 @@ export default defineComponent({
|
|||
type: formData.type.value,
|
||||
color: formData.color.value,
|
||||
nscdr: formData.nscdr.checked,
|
||||
passphrase: formData.passphrase.value,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.updateActiveCalendarType(updateCalendarType)
|
||||
|
|
12
src/views/public/View.vue
Normal file
12
src/views/public/View.vue
Normal file
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<FullContent>
|
||||
<template #main>
|
||||
<RouterView />
|
||||
</template>
|
||||
</FullContent>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from "vue-router";
|
||||
import FullContent from "../../layouts/FullContent.vue";
|
||||
</script>
|
97
src/views/public/calendar/Calendar.vue
Normal file
97
src/views/public/calendar/Calendar.vue
Normal file
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<MainTemplate :showBack="false">
|
||||
<template #topBar>
|
||||
<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>
|
||||
<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>
|
||||
</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" />
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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/calendar.models";
|
||||
import { RouterLink } from "vue-router";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
calendars: [] as Array<CalendarViewModel>,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
formattedItems() {
|
||||
return this.calendars.map((c) => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
start: c.starttime,
|
||||
end: c.endtime,
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCalendars();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
fetchCalendars() {
|
||||
this.$http
|
||||
.get("/public/calendar?output=json")
|
||||
.then((result) => {
|
||||
this.calendars = result.data;
|
||||
})
|
||||
.catch((err) => {});
|
||||
},
|
||||
openLinkModal(e: any) {
|
||||
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/public/calendar/CalendarLinkModal.vue"))));
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
67
src/views/public/calendar/CalendarExplain.vue
Normal file
67
src/views/public/calendar/CalendarExplain.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<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-fit">Kalender-Erklärung - WebCal Kalendar einbinden</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<div class="instruction">
|
||||
<h2>iOS: Webcal-Link einbinden</h2>
|
||||
<ol>
|
||||
<li>Öffne die <strong>"Einstellungen"</strong> auf deinem iPhone oder iPad.</li>
|
||||
<li>Scrolle nach unten und wähle <strong>"Kalender"</strong>.</li>
|
||||
<li>Tippe auf <strong>"Accounts"</strong> und dann auf <strong>"Account hinzufügen"</strong>.</li>
|
||||
<li>Wähle <strong>"Andere"</strong> und anschließend <strong>"Kalender abonnieren"</strong>.</li>
|
||||
<li>Gib den <strong>Webcal-Link</strong> in das Feld ein und tippe auf <strong>"Weiter"</strong>.</li>
|
||||
<li>Der Kalender wird nun hinzugefügt und im Standard-Kalender angezeigt.</li>
|
||||
</ol>
|
||||
<!-- <img src="images/ios_calendar_step1.png" alt="iOS Kalender Einstellungen" /> -->
|
||||
<!-- <img src="images/ios_calendar_step2.png" alt="Webcal-Link eingeben" /> -->
|
||||
</div>
|
||||
<br />
|
||||
<div class="instruction">
|
||||
<h2>Android: Webcal-Link einbinden</h2>
|
||||
<p>
|
||||
Auf Android-Geräten musst du den Webcal-Link über den Google Kalender hinzufügen, da es keine direkte Option
|
||||
gibt:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Öffne auf einem Computer oder im Browser den
|
||||
<strong> <a href="https://calendar.google.com" target="_blank">Google Kalender</a> </strong>.
|
||||
</li>
|
||||
<li>Klicke auf das <strong>"+"-Symbol</strong> neben "Weitere Kalender" (links im Menü).</li>
|
||||
<li>Wähle <strong>"Per URL"</strong> aus.</li>
|
||||
<li>Gib den <strong>Webcal-Link</strong> ein und klicke auf <strong>"Kalender hinzufügen"</strong>.</li>
|
||||
<li>Öffne die Google Kalender App auf deinem Android-Gerät und synchronisiere die Änderungen.</li>
|
||||
</ol>
|
||||
<!-- <img src="images/android_google_calendar_step1.png" alt="Google Kalender im Browser" /> -->
|
||||
<!-- <img src="images/android_google_calendar_step2.png" alt="Webcal-Link einfügen" /> -->
|
||||
</div>
|
||||
|
||||
<div class="instruction">
|
||||
<h2>Hinweise</h2>
|
||||
<ul>
|
||||
<li>Der Webcal-Link muss korrekt formatiert sein (beginnt mit <strong>webcal://</strong>).</li>
|
||||
<li>Wenn der Kalender nicht sofort angezeigt wird, überprüfe die Synchronisierungseinstellungen.</li>
|
||||
<li>Für Android kann es hilfreich sein, die Google Kalender App zu aktualisieren.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
</script>
|
Loading…
Reference in a new issue