forked from potsda.mn/mobilizon
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>
|
||||
<div class="flex justify-center h-80">
|
||||
<div class="flex justify-center max-h-80">
|
||||
<lazy-image-wrapper :picture="picture" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -12,13 +12,13 @@
|
|||
<lazy-image-wrapper
|
||||
:picture="event.picture"
|
||||
: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 class="title-info-wrapper p-2">
|
||||
<div class="p-2">
|
||||
<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"
|
||||
dir="auto"
|
||||
>
|
||||
|
@ -36,9 +36,13 @@
|
|||
>
|
||||
{{ $t("Cancelled") }}
|
||||
</tag>
|
||||
<tag class="mr-2" variant="warning" size="medium" v-if="event.draft">{{
|
||||
$t("Draft")
|
||||
}}</tag>
|
||||
<tag
|
||||
class="mr-2 font-normal"
|
||||
variant="warning"
|
||||
size="medium"
|
||||
v-if="event.draft"
|
||||
>{{ $t("Draft") }}</tag
|
||||
>
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<inline-address
|
||||
|
@ -173,24 +177,5 @@ withDefaults(
|
|||
// .calendar-icon {
|
||||
// @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>
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
<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">
|
||||
<date-calendar-icon
|
||||
:date="participation.event.beginsOn.toString()"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="" v-bind="$attrs">
|
||||
<div class="h-full w-full">
|
||||
<div ref="wrapper" class="flex-1" v-bind="$attrs">
|
||||
<div class="h-full w-full max-w-100 min-h-[10rem]">
|
||||
<!-- Show the placeholder as background -->
|
||||
<blurhash-img
|
||||
v-if="blurhash"
|
||||
|
@ -13,7 +13,7 @@
|
|||
<!-- Show the real image on the top and fade in after loading -->
|
||||
<img
|
||||
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"
|
||||
alt=""
|
||||
src=""
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue