Improve EventView.vue
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
9fe816ebcf
commit
528744d765
904
js/src/components/Event/EventActionSection.vue
Normal file
904
js/src/components/Event/EventActionSection.vue
Normal file
|
@ -0,0 +1,904 @@
|
||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
<participation-section
|
||||||
|
v-if="event && currentActor && identities && anonymousParticipationConfig"
|
||||||
|
:participation="participations[0]"
|
||||||
|
:event="event"
|
||||||
|
:anonymousParticipation="anonymousParticipation"
|
||||||
|
:currentActor="currentActor"
|
||||||
|
:identities="identities"
|
||||||
|
:anonymousParticipationConfig="anonymousParticipationConfig"
|
||||||
|
@join-event="joinEvent"
|
||||||
|
@join-modal="isJoinModalActive = true"
|
||||||
|
@join-event-with-confirmation="joinEventWithConfirmation"
|
||||||
|
@confirm-leave="confirmLeave"
|
||||||
|
@cancel-anonymous-participation="cancelAnonymousParticipation"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<template v-if="!event?.draft">
|
||||||
|
<p
|
||||||
|
v-if="event?.visibility === EventVisibility.PUBLIC"
|
||||||
|
class="inline-flex gap-1"
|
||||||
|
>
|
||||||
|
<Earth />
|
||||||
|
{{ t("Public event") }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="event?.visibility === EventVisibility.UNLISTED"
|
||||||
|
class="inline-flex gap-1"
|
||||||
|
>
|
||||||
|
<Link />
|
||||||
|
{{ t("Private event") }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-if="!event?.local && organizer?.domain">
|
||||||
|
<a :href="event?.url">
|
||||||
|
<tag>{{ organizer?.domain }}</tag>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<p class="inline-flex gap-1">
|
||||||
|
<TicketConfirmationOutline />
|
||||||
|
<router-link
|
||||||
|
class="participations-link"
|
||||||
|
v-if="canManageEvent && event?.draft === false"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.PARTICIPATIONS,
|
||||||
|
params: { eventId: event.uuid },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- We retire one because of the event creator who is a
|
||||||
|
participant -->
|
||||||
|
<span v-if="maximumAttendeeCapacity">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"{available}/{capacity} available places",
|
||||||
|
{
|
||||||
|
available:
|
||||||
|
maximumAttendeeCapacity -
|
||||||
|
event.participantStats.participant,
|
||||||
|
capacity: maximumAttendeeCapacity,
|
||||||
|
},
|
||||||
|
maximumAttendeeCapacity - event.participantStats.participant
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"No one is participating|One person participating|{going} people participating",
|
||||||
|
{
|
||||||
|
going: event.participantStats.participant,
|
||||||
|
},
|
||||||
|
event.participantStats.participant
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
<span v-else>
|
||||||
|
<span v-if="maximumAttendeeCapacity">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"{available}/{capacity} available places",
|
||||||
|
{
|
||||||
|
available:
|
||||||
|
maximumAttendeeCapacity -
|
||||||
|
(event?.participantStats.participant ?? 0),
|
||||||
|
capacity: maximumAttendeeCapacity,
|
||||||
|
},
|
||||||
|
maximumAttendeeCapacity -
|
||||||
|
(event?.participantStats.participant ?? 0)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"No one is participating|One person participating|{going} people participating",
|
||||||
|
{
|
||||||
|
going: event?.participantStats.participant,
|
||||||
|
},
|
||||||
|
event?.participantStats.participant ?? 0
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<VTooltip v-if="event?.local === false">
|
||||||
|
<HelpCircleOutline :size="16" />
|
||||||
|
<template #popper>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"The actual number of participants may differ, as this event is hosted on another instance."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
</p>
|
||||||
|
<o-dropdown>
|
||||||
|
<template #trigger>
|
||||||
|
<o-button icon-right="dots-horizontal">
|
||||||
|
{{ t("Actions") }}
|
||||||
|
</o-button>
|
||||||
|
</template>
|
||||||
|
<o-dropdown-item
|
||||||
|
aria-role="listitem"
|
||||||
|
has-link
|
||||||
|
v-if="canManageEvent || event?.draft"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="flex gap-1"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.EDIT_EVENT,
|
||||||
|
params: { eventId: event?.uuid },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Pencil />
|
||||||
|
{{ t("Edit") }}
|
||||||
|
</router-link>
|
||||||
|
</o-dropdown-item>
|
||||||
|
<o-dropdown-item
|
||||||
|
aria-role="listitem"
|
||||||
|
has-link
|
||||||
|
v-if="canManageEvent || event?.draft"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="flex gap-1"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.DUPLICATE_EVENT,
|
||||||
|
params: { eventId: event?.uuid },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ContentDuplicate />
|
||||||
|
{{ t("Duplicate") }}
|
||||||
|
</router-link>
|
||||||
|
</o-dropdown-item>
|
||||||
|
<o-dropdown-item
|
||||||
|
aria-role="listitem"
|
||||||
|
v-if="canManageEvent || event?.draft"
|
||||||
|
@click="openDeleteEventModal"
|
||||||
|
@keyup.enter="openDeleteEventModal"
|
||||||
|
><span class="flex gap-1">
|
||||||
|
<Delete />
|
||||||
|
{{ t("Delete") }}
|
||||||
|
</span>
|
||||||
|
</o-dropdown-item>
|
||||||
|
|
||||||
|
<hr
|
||||||
|
role="presentation"
|
||||||
|
class="dropdown-divider"
|
||||||
|
aria-role="o-dropdown-item"
|
||||||
|
v-if="canManageEvent || event?.draft"
|
||||||
|
/>
|
||||||
|
<o-dropdown-item
|
||||||
|
aria-role="listitem"
|
||||||
|
v-if="event?.draft === false"
|
||||||
|
@click="triggerShare()"
|
||||||
|
@keyup.enter="triggerShare()"
|
||||||
|
class="p-1"
|
||||||
|
>
|
||||||
|
<span class="flex gap-1">
|
||||||
|
<Share />
|
||||||
|
{{ t("Share this event") }}
|
||||||
|
</span>
|
||||||
|
</o-dropdown-item>
|
||||||
|
<o-dropdown-item
|
||||||
|
aria-role="listitem"
|
||||||
|
@click="downloadIcsEvent()"
|
||||||
|
@keyup.enter="downloadIcsEvent()"
|
||||||
|
v-if="event?.draft === false"
|
||||||
|
>
|
||||||
|
<span class="flex gap-1">
|
||||||
|
<CalendarPlus />
|
||||||
|
{{ t("Add to my calendar") }}
|
||||||
|
</span>
|
||||||
|
</o-dropdown-item>
|
||||||
|
<o-dropdown-item
|
||||||
|
aria-role="listitem"
|
||||||
|
v-if="ableToReport"
|
||||||
|
@click="isReportModalActive = true"
|
||||||
|
@keyup.enter="isReportModalActive = true"
|
||||||
|
class="p-1"
|
||||||
|
>
|
||||||
|
<span class="flex gap-1">
|
||||||
|
<Flag />
|
||||||
|
{{ t("Report") }}
|
||||||
|
</span>
|
||||||
|
</o-dropdown-item>
|
||||||
|
</o-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<o-modal
|
||||||
|
v-model:active="isReportModalActive"
|
||||||
|
has-modal-card
|
||||||
|
ref="reportModal"
|
||||||
|
:close-button-aria-label="t('Close')"
|
||||||
|
>
|
||||||
|
<ReportModal
|
||||||
|
:on-confirm="reportEvent"
|
||||||
|
:title="t('Report this event')"
|
||||||
|
:outside-domain="organizerDomain"
|
||||||
|
/>
|
||||||
|
</o-modal>
|
||||||
|
<o-modal
|
||||||
|
:close-button-aria-label="t('Close')"
|
||||||
|
v-model:active="isShareModalActive"
|
||||||
|
has-modal-card
|
||||||
|
ref="shareModal"
|
||||||
|
>
|
||||||
|
<share-event-modal
|
||||||
|
v-if="event"
|
||||||
|
:event="event"
|
||||||
|
:eventCapacityOK="eventCapacityOK"
|
||||||
|
/>
|
||||||
|
</o-modal>
|
||||||
|
<o-modal
|
||||||
|
v-model:active="isJoinModalActive"
|
||||||
|
has-modal-card
|
||||||
|
ref="participationModal"
|
||||||
|
:close-button-aria-label="t('Close')"
|
||||||
|
>
|
||||||
|
<identity-picker v-if="identity" v-model="identity">
|
||||||
|
<template #footer>
|
||||||
|
<footer class="flex gap-2">
|
||||||
|
<o-button
|
||||||
|
outlined
|
||||||
|
ref="cancelButton"
|
||||||
|
@click="isJoinModalActive = false"
|
||||||
|
@keyup.enter="isJoinModalActive = false"
|
||||||
|
>
|
||||||
|
{{ t("Cancel") }}
|
||||||
|
</o-button>
|
||||||
|
<o-button
|
||||||
|
v-if="identity"
|
||||||
|
variant="primary"
|
||||||
|
ref="confirmButton"
|
||||||
|
@click="
|
||||||
|
event?.joinOptions === EventJoinOptions.RESTRICTED
|
||||||
|
? joinEventWithConfirmation(identity as IPerson)
|
||||||
|
: joinEvent(identity as IPerson)
|
||||||
|
"
|
||||||
|
@keyup.enter="
|
||||||
|
event?.joinOptions === EventJoinOptions.RESTRICTED
|
||||||
|
? joinEventWithConfirmation(identity as IPerson)
|
||||||
|
: joinEvent(identity as IPerson)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ t("Confirm my particpation") }}
|
||||||
|
</o-button>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
</identity-picker>
|
||||||
|
</o-modal>
|
||||||
|
<o-modal
|
||||||
|
v-model:active="isJoinConfirmationModalActive"
|
||||||
|
has-modal-card
|
||||||
|
ref="joinConfirmationModal"
|
||||||
|
:close-button-aria-label="t('Close')"
|
||||||
|
>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<p class="modal-card-title">
|
||||||
|
{{ t("Participation confirmation") }}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
@submit.prevent="
|
||||||
|
joinEvent(actorForConfirmation as IPerson, messageForConfirmation)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<o-field :label="t('Message')">
|
||||||
|
<o-input
|
||||||
|
type="textarea"
|
||||||
|
size="medium"
|
||||||
|
v-model="messageForConfirmation"
|
||||||
|
minlength="10"
|
||||||
|
></o-input>
|
||||||
|
</o-field>
|
||||||
|
<div class="buttons">
|
||||||
|
<o-button
|
||||||
|
native-type="button"
|
||||||
|
class="button"
|
||||||
|
ref="cancelButton"
|
||||||
|
@click="isJoinConfirmationModalActive = false"
|
||||||
|
@keyup.enter="isJoinConfirmationModalActive = false"
|
||||||
|
>{{ t("Cancel") }}
|
||||||
|
</o-button>
|
||||||
|
<o-button variant="primary" native-type="submit">
|
||||||
|
{{ t("Confirm my participation") }}
|
||||||
|
</o-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</o-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { IActor, IPerson } from "@/types/actor";
|
||||||
|
import { IEvent } from "@/types/event.model";
|
||||||
|
import ParticipationSection from "@/components/Participation/ParticipationSection.vue";
|
||||||
|
import ReportModal from "@/components/Report/ReportModal.vue";
|
||||||
|
import IdentityPicker from "@/views/Account/IdentityPicker.vue";
|
||||||
|
import {
|
||||||
|
EventVisibility,
|
||||||
|
EventJoinOptions,
|
||||||
|
ParticipantRole,
|
||||||
|
MemberRole,
|
||||||
|
} from "@/types/enums";
|
||||||
|
import { GRAPHQL_API_ENDPOINT } from "@/api/_entrypoint";
|
||||||
|
import { computed, defineAsyncComponent, inject, onMounted, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import Tag from "@/components/TagElement.vue";
|
||||||
|
import Earth from "vue-material-design-icons/Earth.vue";
|
||||||
|
import Link from "vue-material-design-icons/Link.vue";
|
||||||
|
import Flag from "vue-material-design-icons/Flag.vue";
|
||||||
|
import CalendarPlus from "vue-material-design-icons/CalendarPlus.vue";
|
||||||
|
import ContentDuplicate from "vue-material-design-icons/ContentDuplicate.vue";
|
||||||
|
import Delete from "vue-material-design-icons/Delete.vue";
|
||||||
|
import Pencil from "vue-material-design-icons/Pencil.vue";
|
||||||
|
import HelpCircleOutline from "vue-material-design-icons/HelpCircleOutline.vue";
|
||||||
|
import TicketConfirmationOutline from "vue-material-design-icons/TicketConfirmationOutline.vue";
|
||||||
|
import Share from "vue-material-design-icons/Share.vue";
|
||||||
|
import {
|
||||||
|
EVENT_PERSON_PARTICIPATION,
|
||||||
|
FETCH_EVENT,
|
||||||
|
JOIN_EVENT,
|
||||||
|
LEAVE_EVENT,
|
||||||
|
} from "@/graphql/event";
|
||||||
|
import { Notifier } from "@/plugins/notifier";
|
||||||
|
import { Dialog } from "@/plugins/dialog";
|
||||||
|
import { Snackbar } from "@/plugins/snackbar";
|
||||||
|
import RouteName from "@/router/name";
|
||||||
|
import {
|
||||||
|
AnonymousParticipationNotFoundError,
|
||||||
|
getLeaveTokenForParticipation,
|
||||||
|
isParticipatingInThisEvent,
|
||||||
|
removeAnonymousParticipation,
|
||||||
|
} from "@/services/AnonymousParticipationStorage";
|
||||||
|
import {
|
||||||
|
useAnonymousActorId,
|
||||||
|
useAnonymousParticipationConfig,
|
||||||
|
useAnonymousReportsConfig,
|
||||||
|
} from "@/composition/apollo/config";
|
||||||
|
import { useCurrentUserIdentities } from "@/composition/apollo/actor";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { IParticipant } from "@/types/participant.model";
|
||||||
|
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||||
|
import { useMutation } from "@vue/apollo-composable";
|
||||||
|
import { useCreateReport } from "@/composition/apollo/report";
|
||||||
|
import { useDeleteEvent } from "@/composition/apollo/event";
|
||||||
|
|
||||||
|
const ShareEventModal = defineAsyncComponent(
|
||||||
|
() => import("@/components/Event/ShareEventModal.vue")
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
event: IEvent;
|
||||||
|
currentActor: IPerson | undefined;
|
||||||
|
participations: IParticipant[];
|
||||||
|
person: IPerson | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
const notifier = inject<Notifier>("notifier");
|
||||||
|
const dialog = inject<Dialog>("dialog");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { anonymousReportsConfig } = useAnonymousReportsConfig();
|
||||||
|
const { anonymousActorId } = useAnonymousActorId();
|
||||||
|
const { anonymousParticipationConfig } = useAnonymousParticipationConfig();
|
||||||
|
const { identities } = useCurrentUserIdentities();
|
||||||
|
|
||||||
|
const event = computed(() => props.event);
|
||||||
|
|
||||||
|
const identity = ref<IPerson | undefined | null>(null);
|
||||||
|
|
||||||
|
const ableToReport = computed((): boolean => {
|
||||||
|
return (
|
||||||
|
props.currentActor?.id != null ||
|
||||||
|
anonymousReportsConfig.value?.allowed === true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const organizer = computed((): IActor | null => {
|
||||||
|
if (event.value?.attributedTo?.id) {
|
||||||
|
return event.value.attributedTo;
|
||||||
|
}
|
||||||
|
if (event.value?.organizerActor) {
|
||||||
|
return event.value.organizerActor;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const organizerDomain = computed((): string | undefined => {
|
||||||
|
return organizer.value?.domain ?? undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const reportModal = ref();
|
||||||
|
const isReportModalActive = ref(false);
|
||||||
|
const isShareModalActive = ref(false);
|
||||||
|
const isJoinModalActive = ref(false);
|
||||||
|
const isJoinConfirmationModalActive = ref(false);
|
||||||
|
|
||||||
|
const actorForConfirmation = ref<IPerson | null>(null);
|
||||||
|
const messageForConfirmation = ref("");
|
||||||
|
|
||||||
|
const anonymousParticipation = ref<boolean | null>(null);
|
||||||
|
|
||||||
|
const downloadIcsEvent = async (): Promise<void> => {
|
||||||
|
const data = await (
|
||||||
|
await fetch(`${GRAPHQL_API_ENDPOINT}/events/${event.value.uuid}/export/ics`)
|
||||||
|
).text();
|
||||||
|
const blob = new Blob([data], { type: "text/calendar" });
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = `${event.value?.title}.ics`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerShare = (): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore-start
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
.share({
|
||||||
|
title: event.value?.title,
|
||||||
|
url: event.value?.url,
|
||||||
|
})
|
||||||
|
.then(() => console.debug("Successful share"))
|
||||||
|
.catch((error: any) => console.debug("Error sharing", error));
|
||||||
|
} else {
|
||||||
|
isShareModalActive.value = true;
|
||||||
|
// send popup
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore-end
|
||||||
|
};
|
||||||
|
|
||||||
|
const canManageEvent = computed((): boolean => {
|
||||||
|
return actorIsOrganizer.value || hasGroupPrivileges.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// const actorIsParticipant = computed((): boolean => {
|
||||||
|
// if (actorIsOrganizer.value) return true;
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// participations.value.length > 0 &&
|
||||||
|
// participations.value[0].role === ParticipantRole.PARTICIPANT
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
const actorIsOrganizer = computed((): boolean => {
|
||||||
|
return (
|
||||||
|
props.participations.length > 0 &&
|
||||||
|
props.participations[0].role === ParticipantRole.CREATOR
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasGroupPrivileges = computed((): boolean => {
|
||||||
|
return (
|
||||||
|
props.person?.memberships !== undefined &&
|
||||||
|
props.person?.memberships?.total > 0 &&
|
||||||
|
[MemberRole.MODERATOR, MemberRole.ADMINISTRATOR].includes(
|
||||||
|
props.person?.memberships?.elements[0].role
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinEventWithConfirmation = (actor: IPerson): void => {
|
||||||
|
isJoinConfirmationModalActive.value = true;
|
||||||
|
actorForConfirmation.value = actor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: joinEventMutation,
|
||||||
|
onDone: onJoinEventMutationDone,
|
||||||
|
onError: onJoinEventMutationError,
|
||||||
|
} = useMutation<{
|
||||||
|
joinEvent: IParticipant;
|
||||||
|
}>(JOIN_EVENT, () => ({
|
||||||
|
update: (
|
||||||
|
store: ApolloCache<{
|
||||||
|
joinEvent: IParticipant;
|
||||||
|
}>,
|
||||||
|
{ data }: FetchResult
|
||||||
|
) => {
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
const participationCachedData = store.readQuery<{ person: IPerson }>({
|
||||||
|
query: EVENT_PERSON_PARTICIPATION,
|
||||||
|
variables: { eventId: event.value?.id, actorId: identity.value?.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (participationCachedData?.person == undefined) {
|
||||||
|
console.error(
|
||||||
|
"Cannot update participation cache, because of null value."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.writeQuery({
|
||||||
|
query: EVENT_PERSON_PARTICIPATION,
|
||||||
|
variables: { eventId: event.value?.id, actorId: identity.value?.id },
|
||||||
|
data: {
|
||||||
|
person: {
|
||||||
|
...participationCachedData?.person,
|
||||||
|
participations: {
|
||||||
|
elements: [data.joinEvent],
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cachedData = store.readQuery<{ event: IEvent }>({
|
||||||
|
query: FETCH_EVENT,
|
||||||
|
variables: { uuid: event.value?.uuid },
|
||||||
|
});
|
||||||
|
if (cachedData == null) return;
|
||||||
|
const { event: cachedEvent } = cachedData;
|
||||||
|
if (cachedEvent === null) {
|
||||||
|
console.error(
|
||||||
|
"Cannot update event participant cache, because of null value."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const participantStats = { ...cachedEvent.participantStats };
|
||||||
|
|
||||||
|
if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
|
||||||
|
participantStats.notApproved += 1;
|
||||||
|
} else {
|
||||||
|
participantStats.going += 1;
|
||||||
|
participantStats.participant += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.writeQuery({
|
||||||
|
query: FETCH_EVENT,
|
||||||
|
variables: { uuid: props.event.uuid },
|
||||||
|
data: {
|
||||||
|
event: {
|
||||||
|
...cachedEvent,
|
||||||
|
participantStats,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const joinEvent = (
|
||||||
|
identityForJoin: IPerson,
|
||||||
|
message: string | null = null
|
||||||
|
): void => {
|
||||||
|
isJoinConfirmationModalActive.value = false;
|
||||||
|
isJoinModalActive.value = false;
|
||||||
|
joinEventMutation({
|
||||||
|
eventId: event.value?.id,
|
||||||
|
actorId: identityForJoin?.id,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const participationRequestedMessage = () => {
|
||||||
|
notifier?.success(t("Your participation has been requested"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const participationConfirmedMessage = () => {
|
||||||
|
notifier?.success(t("Your participation has been confirmed"));
|
||||||
|
};
|
||||||
|
|
||||||
|
onJoinEventMutationDone(({ data }) => {
|
||||||
|
if (data) {
|
||||||
|
if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
|
||||||
|
participationRequestedMessage();
|
||||||
|
} else {
|
||||||
|
participationConfirmedMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onJoinEventMutationError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmLeave = (): void => {
|
||||||
|
dialog?.confirm({
|
||||||
|
title: t('Leaving event "{title}"', {
|
||||||
|
title: event.value?.title,
|
||||||
|
}),
|
||||||
|
message: t(
|
||||||
|
'Are you sure you want to cancel your participation at event "{title}"?',
|
||||||
|
{
|
||||||
|
title: event.value?.title,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
confirmText: t("Leave event"),
|
||||||
|
cancelText: t("Cancel"),
|
||||||
|
variant: "danger",
|
||||||
|
hasIcon: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
if (event.value && props.currentActor?.id) {
|
||||||
|
console.debug("calling leave event");
|
||||||
|
leaveEvent(event.value, props.currentActor.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: createReportMutation,
|
||||||
|
onDone: onCreateReportDone,
|
||||||
|
onError: onCreateReportError,
|
||||||
|
} = useCreateReport();
|
||||||
|
|
||||||
|
onCreateReportDone(() => {
|
||||||
|
notifier?.success(
|
||||||
|
t("Event {eventTitle} reported", { eventTitle: props?.event?.title })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCreateReportError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reportEvent = async (
|
||||||
|
content: string,
|
||||||
|
forward: boolean
|
||||||
|
): Promise<void> => {
|
||||||
|
isReportModalActive.value = false;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
reportModal.value.close();
|
||||||
|
if (!organizer.value) return;
|
||||||
|
|
||||||
|
createReportMutation({
|
||||||
|
eventId: event.value?.id ?? "",
|
||||||
|
reportedId: organizer.value?.id ?? "",
|
||||||
|
content,
|
||||||
|
forward,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const maximumAttendeeCapacity = computed((): number | undefined => {
|
||||||
|
return event.value?.options?.maximumAttendeeCapacity;
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventCapacityOK = computed((): boolean => {
|
||||||
|
if (event.value?.draft) return true;
|
||||||
|
if (!maximumAttendeeCapacity.value) return true;
|
||||||
|
return (
|
||||||
|
event.value?.options?.maximumAttendeeCapacity !== undefined &&
|
||||||
|
event.value.participantStats.participant !== undefined &&
|
||||||
|
event.value?.options?.maximumAttendeeCapacity >
|
||||||
|
event.value.participantStats.participant
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// const numberOfPlacesStillAvailable = computed((): number | undefined => {
|
||||||
|
// if (event.value?.draft) return maximumAttendeeCapacity.value;
|
||||||
|
// return (
|
||||||
|
// (maximumAttendeeCapacity.value ?? 0) -
|
||||||
|
// (event.value?.participantStats.participant ?? 0)
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: leaveEventMutation,
|
||||||
|
onDone: onLeaveEventMutationDone,
|
||||||
|
onError: onLeaveEventMutationError,
|
||||||
|
} = useMutation<{ leaveEvent: { actor: { id: string } } }>(LEAVE_EVENT, () => ({
|
||||||
|
update: (
|
||||||
|
store: ApolloCache<{
|
||||||
|
leaveEvent: IParticipant;
|
||||||
|
}>,
|
||||||
|
{ data }: FetchResult,
|
||||||
|
{ context, variables }
|
||||||
|
) => {
|
||||||
|
if (data == null) return;
|
||||||
|
let participation;
|
||||||
|
|
||||||
|
const token = context?.token;
|
||||||
|
const actorId = variables?.actorId;
|
||||||
|
const localEventId = variables?.eventId;
|
||||||
|
const eventUUID = context?.eventUUID;
|
||||||
|
const isAnonymousParticipationConfirmed =
|
||||||
|
context?.isAnonymousParticipationConfirmed;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
const participationCachedData = store.readQuery<{
|
||||||
|
person: IPerson;
|
||||||
|
}>({
|
||||||
|
query: EVENT_PERSON_PARTICIPATION,
|
||||||
|
variables: { eventId: localEventId, actorId },
|
||||||
|
});
|
||||||
|
if (participationCachedData == null) return;
|
||||||
|
const { person: cachedPerson } = participationCachedData;
|
||||||
|
[participation] = cachedPerson?.participations?.elements ?? [undefined];
|
||||||
|
|
||||||
|
store.modify({
|
||||||
|
id: `Person:${actorId}`,
|
||||||
|
fields: {
|
||||||
|
participations() {
|
||||||
|
return {
|
||||||
|
elements: [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventCachedData = store.readQuery<{ event: IEvent }>({
|
||||||
|
query: FETCH_EVENT,
|
||||||
|
variables: { uuid: eventUUID },
|
||||||
|
});
|
||||||
|
if (eventCachedData == null) return;
|
||||||
|
const { event: eventCached } = eventCachedData;
|
||||||
|
if (eventCached === null) {
|
||||||
|
console.error("Cannot update event cache, because of null value.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const participantStats = { ...eventCached.participantStats };
|
||||||
|
if (participation && participation?.role === ParticipantRole.NOT_APPROVED) {
|
||||||
|
participantStats.notApproved -= 1;
|
||||||
|
} else if (isAnonymousParticipationConfirmed === false) {
|
||||||
|
participantStats.notConfirmed -= 1;
|
||||||
|
} else {
|
||||||
|
participantStats.going -= 1;
|
||||||
|
participantStats.participant -= 1;
|
||||||
|
}
|
||||||
|
store.writeQuery({
|
||||||
|
query: FETCH_EVENT,
|
||||||
|
variables: { uuid: eventUUID },
|
||||||
|
data: {
|
||||||
|
event: {
|
||||||
|
...eventCached,
|
||||||
|
participantStats,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const leaveEvent = (
|
||||||
|
eventToLeave: IEvent,
|
||||||
|
actorId: string,
|
||||||
|
token: string | null = null,
|
||||||
|
isAnonymousParticipationConfirmed: boolean | null = null
|
||||||
|
): void => {
|
||||||
|
leaveEventMutation(
|
||||||
|
{
|
||||||
|
eventId: eventToLeave.id,
|
||||||
|
actorId,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
token,
|
||||||
|
isAnonymousParticipationConfirmed,
|
||||||
|
eventUUID: eventToLeave.uuid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onLeaveEventMutationDone(({ data }) => {
|
||||||
|
if (data) {
|
||||||
|
notifier?.success(t("You have cancelled your participation"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const snackbar = inject<Snackbar>("snackbar");
|
||||||
|
|
||||||
|
onLeaveEventMutationError((error) => {
|
||||||
|
snackbar?.open({
|
||||||
|
message: error.message,
|
||||||
|
variant: "danger",
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const anonymousParticipationConfirmed = async (): Promise<boolean> => {
|
||||||
|
return isParticipatingInThisEvent(props.event?.uuid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelAnonymousParticipation = async (): Promise<void> => {
|
||||||
|
if (!event.value || !anonymousActorId.value) return;
|
||||||
|
const token = (await getLeaveTokenForParticipation(
|
||||||
|
props.event?.uuid
|
||||||
|
)) as string;
|
||||||
|
leaveEvent(event.value, anonymousActorId.value, token);
|
||||||
|
await removeAnonymousParticipation(props.event?.uuid);
|
||||||
|
anonymousParticipation.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
identity.value = props.currentActor;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.isSecureContext) {
|
||||||
|
anonymousParticipation.value = await anonymousParticipationConfirmed();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof AnonymousParticipationNotFoundError) {
|
||||||
|
anonymousParticipation.value = null;
|
||||||
|
} else {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: deleteEvent,
|
||||||
|
onDone: onDeleteEventDone,
|
||||||
|
onError: onDeleteEventError,
|
||||||
|
} = useDeleteEvent();
|
||||||
|
|
||||||
|
const escapeRegExp = (string: string) => {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEventMessage = computed(() => {
|
||||||
|
const participantsLength = event.value?.participantStats.participant;
|
||||||
|
const prefix = participantsLength
|
||||||
|
? t(
|
||||||
|
"There are {participants} participants.",
|
||||||
|
{
|
||||||
|
participants: event.value.participantStats.participant,
|
||||||
|
},
|
||||||
|
event.value.participantStats.participant
|
||||||
|
)
|
||||||
|
: "";
|
||||||
|
return `${prefix}
|
||||||
|
${t(
|
||||||
|
"Are you sure you want to delete this event? This action cannot be reverted."
|
||||||
|
)}
|
||||||
|
<br><br>
|
||||||
|
${t('To confirm, type your event title "{eventTitle}"', {
|
||||||
|
eventTitle: event.value?.title,
|
||||||
|
})}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDeleteEventModal = () => {
|
||||||
|
dialog?.prompt({
|
||||||
|
title: t("Delete event"),
|
||||||
|
message: deleteEventMessage.value,
|
||||||
|
confirmText: t("Delete event"),
|
||||||
|
cancelText: t("Cancel"),
|
||||||
|
variant: "danger",
|
||||||
|
hasIcon: true,
|
||||||
|
hasInput: true,
|
||||||
|
inputAttrs: {
|
||||||
|
placeholder: event.value?.title,
|
||||||
|
pattern: escapeRegExp(event.value?.title ?? ""),
|
||||||
|
},
|
||||||
|
onConfirm: (result: string) => {
|
||||||
|
console.debug("calling delete event", result);
|
||||||
|
if (result.trim() === event.value?.title) {
|
||||||
|
event.value?.id ? deleteEvent({ eventId: event.value?.id }) : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteEventDone(() => {
|
||||||
|
router.push({ name: RouteName.MY_EVENTS });
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeleteEventError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center h-80">
|
<div class="flex justify-center max-h-80">
|
||||||
<lazy-image-wrapper :picture="picture" />
|
<lazy-image-wrapper :picture="picture" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -12,13 +12,13 @@
|
||||||
<lazy-image-wrapper
|
<lazy-image-wrapper
|
||||||
:picture="event.picture"
|
:picture="event.picture"
|
||||||
:rounded="true"
|
:rounded="true"
|
||||||
class="object-cover flex-none h-32 md:w-48 rounded-t-lg md:rounded-none md:rounded-l-lg"
|
class="object-cover flex-none h-40 md:w-48 rounded-t-lg md:rounded-none md:rounded-l-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="title-info-wrapper p-2">
|
<div class="p-2">
|
||||||
<h3
|
<h3
|
||||||
class="event-minimalist-title pb-2 text-lg leading-6 line-clamp-3 font-bold text-violet-title dark:text-white"
|
class="pb-2 text-lg leading-6 line-clamp-3 font-bold text-violet-title dark:text-white"
|
||||||
:lang="event.language"
|
:lang="event.language"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
|
@ -36,9 +36,13 @@
|
||||||
>
|
>
|
||||||
{{ $t("Cancelled") }}
|
{{ $t("Cancelled") }}
|
||||||
</tag>
|
</tag>
|
||||||
<tag class="mr-2" variant="warning" size="medium" v-if="event.draft">{{
|
<tag
|
||||||
$t("Draft")
|
class="mr-2 font-normal"
|
||||||
}}</tag>
|
variant="warning"
|
||||||
|
size="medium"
|
||||||
|
v-if="event.draft"
|
||||||
|
>{{ $t("Draft") }}</tag
|
||||||
|
>
|
||||||
{{ event.title }}
|
{{ event.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<inline-address
|
<inline-address
|
||||||
|
@ -173,24 +177,5 @@ withDefaults(
|
||||||
// .calendar-icon {
|
// .calendar-icon {
|
||||||
// @include margin-right(1rem);
|
// @include margin-right(1rem);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
.title-info-wrapper {
|
|
||||||
flex: 2;
|
|
||||||
|
|
||||||
// .event-minimalist-title {
|
|
||||||
// padding-bottom: 5px;
|
|
||||||
// font-size: 18px;
|
|
||||||
// line-height: 24px;
|
|
||||||
// display: -webkit-box;
|
|
||||||
// -webkit-line-clamp: 3;
|
|
||||||
// -webkit-box-orient: vertical;
|
|
||||||
// overflow: hidden;
|
|
||||||
// font-weight: bold;
|
|
||||||
d // }
|
|
||||||
|
|
||||||
:deep(.icon) {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-x-1.5 md:gap-y-3 gapt-x-3"
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-x-1.5 md:gap-y-3 gapt-x-3"
|
||||||
>
|
>
|
||||||
<div class="mr-0 ml-0">
|
<div class="mr-0 ml-0">
|
||||||
<div class="h-36 relative w-full">
|
<div class="h-40 relative w-full">
|
||||||
<div class="flex absolute bottom-2 left-2 z-10">
|
<div class="flex absolute bottom-2 left-2 z-10">
|
||||||
<date-calendar-icon
|
<date-calendar-icon
|
||||||
:date="participation.event.beginsOn.toString()"
|
:date="participation.event.beginsOn.toString()"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="" v-bind="$attrs">
|
<div ref="wrapper" class="flex-1" v-bind="$attrs">
|
||||||
<div class="h-full w-full">
|
<div class="h-full w-full max-w-100 min-h-[10rem]">
|
||||||
<!-- Show the placeholder as background -->
|
<!-- Show the placeholder as background -->
|
||||||
<blurhash-img
|
<blurhash-img
|
||||||
v-if="blurhash"
|
v-if="blurhash"
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<!-- Show the real image on the top and fade in after loading -->
|
<!-- Show the real image on the top and fade in after loading -->
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
class="transition-opacity duration-500 rounded-lg object-cover w-full h-full"
|
class="transition-opacity duration-500 rounded-lg object-cover mx-auto h-full"
|
||||||
:class="imageOpacity"
|
:class="imageOpacity"
|
||||||
alt=""
|
alt=""
|
||||||
src=""
|
src=""
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue