Merge branch 'fixes' into 'main'
Fixes See merge request framasoft/mobilizon!1286
This commit is contained in:
commit
68378c1860
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=""
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
import { LocationType } from "../../types/user-location.model";
|
import { LocationType } from "../../types/user-location.model";
|
||||||
import MoreContent from "./MoreContent.vue";
|
import MoreContent from "./MoreContent.vue";
|
||||||
import CloseContent from "./CloseContent.vue";
|
import CloseContent from "./CloseContent.vue";
|
||||||
import { computed, onMounted, ref, useAttrs } from "vue";
|
import { computed, onMounted, useAttrs } from "vue";
|
||||||
import { SEARCH_EVENTS } from "@/graphql/search";
|
import { SEARCH_EVENTS } from "@/graphql/search";
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent } from "@/types/event.model";
|
||||||
import { useLazyQuery } from "@vue/apollo-composable";
|
import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('bold') }"
|
:class="{ 'is-active': editor?.isActive('bold') }"
|
||||||
@click="editor?.chain().focus().toggleBold().run()"
|
@click="editor?.chain().focus().toggleBold().run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Bold')"
|
:title="t('Bold')"
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('italic') }"
|
:class="{ 'is-active': editor?.isActive('italic') }"
|
||||||
@click="editor?.chain().focus().toggleItalic().run()"
|
@click="editor?.chain().focus().toggleItalic().run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Italic')"
|
:title="t('Italic')"
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('underline') }"
|
:class="{ 'is-active': editor?.isActive('underline') }"
|
||||||
@click="editor?.chain().focus().toggleUnderline().run()"
|
@click="editor?.chain().focus().toggleUnderline().run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Underline')"
|
:title="t('Underline')"
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
<button
|
<button
|
||||||
v-if="!isBasicMode"
|
v-if="!isBasicMode"
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
:class="{ 'is-active': editor?.isActive('heading', { level: 1 }) }"
|
||||||
@click="editor?.chain().focus().toggleHeading({ level: 1 }).run()"
|
@click="editor?.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Heading Level 1')"
|
:title="t('Heading Level 1')"
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
<button
|
<button
|
||||||
v-if="!isBasicMode"
|
v-if="!isBasicMode"
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
:class="{ 'is-active': editor?.isActive('heading', { level: 2 }) }"
|
||||||
@click="editor?.chain().focus().toggleHeading({ level: 2 }).run()"
|
@click="editor?.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Heading Level 2')"
|
:title="t('Heading Level 2')"
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
<button
|
<button
|
||||||
v-if="!isBasicMode"
|
v-if="!isBasicMode"
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
:class="{ 'is-active': editor?.isActive('heading', { level: 3 }) }"
|
||||||
@click="editor?.chain().focus().toggleHeading({ level: 3 }).run()"
|
@click="editor?.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Heading Level 3')"
|
:title="t('Heading Level 3')"
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="showLinkMenu()"
|
@click="showLinkMenu()"
|
||||||
:class="{ 'is-active': editor.isActive('link') }"
|
:class="{ 'is-active': editor?.isActive('link') }"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Add link')"
|
:title="t('Add link')"
|
||||||
>
|
>
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="editor.isActive('link')"
|
v-if="editor?.isActive('link')"
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="editor?.chain().focus().unsetLink().run()"
|
@click="editor?.chain().focus().unsetLink().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
v-if="!isBasicMode"
|
v-if="!isBasicMode"
|
||||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
:class="{ 'is-active': editor?.isActive('bulletList') }"
|
||||||
@click="editor?.chain().focus().toggleBulletList().run()"
|
@click="editor?.chain().focus().toggleBulletList().run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Bullet list')"
|
:title="t('Bullet list')"
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
<button
|
<button
|
||||||
v-if="!isBasicMode"
|
v-if="!isBasicMode"
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
:class="{ 'is-active': editor?.isActive('orderedList') }"
|
||||||
@click="editor?.chain().focus().toggleOrderedList().run()"
|
@click="editor?.chain().focus().toggleOrderedList().run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Ordered list')"
|
:title="t('Ordered list')"
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
<button
|
<button
|
||||||
v-if="!isBasicMode"
|
v-if="!isBasicMode"
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
:class="{ 'is-active': editor?.isActive('blockquote') }"
|
||||||
@click="editor?.chain().focus().toggleBlockquote().run()"
|
@click="editor?.chain().focus().toggleBlockquote().run()"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('Quote')"
|
:title="t('Quote')"
|
||||||
|
@ -193,7 +193,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
|
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
|
||||||
import Blockquote from "@tiptap/extension-blockquote";
|
import Blockquote from "@tiptap/extension-blockquote";
|
||||||
import BulletList from "@tiptap/extension-bullet-list";
|
import BulletList from "@tiptap/extension-bullet-list";
|
||||||
import Heading from "@tiptap/extension-heading";
|
import Heading from "@tiptap/extension-heading";
|
||||||
|
@ -218,7 +218,7 @@ import Underline from "@tiptap/extension-underline";
|
||||||
import Link from "@tiptap/extension-link";
|
import Link from "@tiptap/extension-link";
|
||||||
import { AutoDir } from "./Editor/Autodir";
|
import { AutoDir } from "./Editor/Autodir";
|
||||||
// import sanitizeHtml from "sanitize-html";
|
// import sanitizeHtml from "sanitize-html";
|
||||||
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { computed, inject, onBeforeUnmount, watch } from "vue";
|
||||||
import { Dialog } from "@/plugins/dialog";
|
import { Dialog } from "@/plugins/dialog";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useMutation } from "@vue/apollo-composable";
|
import { useMutation } from "@vue/apollo-composable";
|
||||||
|
@ -254,8 +254,6 @@ const props = withDefaults(
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
const editor = ref<Editor | null>(null);
|
|
||||||
|
|
||||||
const isDescriptionMode = computed((): boolean => {
|
const isDescriptionMode = computed((): boolean => {
|
||||||
return props.mode === "description" || isBasicMode.value;
|
return props.mode === "description" || isBasicMode.value;
|
||||||
});
|
});
|
||||||
|
@ -278,13 +276,28 @@ const isBasicMode = computed((): boolean => {
|
||||||
|
|
||||||
// const observer = ref<MutationObserver | null>(null);
|
// const observer = ref<MutationObserver | null>(null);
|
||||||
|
|
||||||
onMounted(() => {
|
const transformPastedHTML = (html: string): string => {
|
||||||
editor.value = new Editor({
|
// When using comment mode, limit to acceptable tags
|
||||||
|
if (isCommentMode.value) {
|
||||||
|
// return sanitizeHtml(html, {
|
||||||
|
// allowedTags: ["b", "i", "em", "strong", "a"],
|
||||||
|
// allowedAttributes: {
|
||||||
|
// a: ["href", "rel", "target"],
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: {
|
attributes: {
|
||||||
"aria-multiline": isShortMode.value.toString(),
|
"aria-multiline": isShortMode.value.toString(),
|
||||||
"aria-label": props.ariaLabel ?? "",
|
"aria-label": props.ariaLabel ?? "",
|
||||||
role: "textbox",
|
role: "textbox",
|
||||||
|
class:
|
||||||
|
"prose dark:prose-invert prose-sm sm:prose lg:prose-lg xl:prose-xl m-5 focus:outline-none !max-w-full",
|
||||||
},
|
},
|
||||||
transformPastedHTML: transformPastedHTML,
|
transformPastedHTML: transformPastedHTML,
|
||||||
},
|
},
|
||||||
|
@ -317,21 +330,6 @@ onMounted(() => {
|
||||||
emit("update:modelValue", editor.value?.getHTML());
|
emit("update:modelValue", editor.value?.getHTML());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const transformPastedHTML = (html: string): string => {
|
|
||||||
// When using comment mode, limit to acceptable tags
|
|
||||||
if (isCommentMode.value) {
|
|
||||||
// return sanitizeHtml(html, {
|
|
||||||
// allowedTags: ["b", "i", "em", "strong", "a"],
|
|
||||||
// allowedAttributes: {
|
|
||||||
// a: ["href", "rel", "target"],
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
const value = computed(() => props.modelValue);
|
const value = computed(() => props.modelValue);
|
||||||
|
|
||||||
|
@ -351,6 +349,7 @@ const { t } = useI18n({ useScope: "global" });
|
||||||
const showLinkMenu = (): void => {
|
const showLinkMenu = (): void => {
|
||||||
dialog?.prompt({
|
dialog?.prompt({
|
||||||
message: t("Enter the link URL"),
|
message: t("Enter the link URL"),
|
||||||
|
hasInput: true,
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: "url",
|
type: "url",
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const VALIDATE_USER = gql`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LOGGED_USER = gql`
|
export const LOGGED_USER = gql`
|
||||||
query {
|
query LoggedUserQuery {
|
||||||
loggedUser {
|
loggedUser {
|
||||||
id
|
id
|
||||||
email
|
email
|
||||||
|
@ -300,7 +300,7 @@ export const UPDATE_USER_LOCALE = gql`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FEED_TOKENS_LOGGED_USER = gql`
|
export const FEED_TOKENS_LOGGED_USER = gql`
|
||||||
query {
|
query FeedTokensLoggedUser {
|
||||||
loggedUser {
|
loggedUser {
|
||||||
id
|
id
|
||||||
feedTokens {
|
feedTokens {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto py-4 md:py-12 px-2 md:px-60">
|
<div class="container mx-auto py-4 md:py-12 px-2 md:px-60">
|
||||||
<main>
|
<main>
|
||||||
<div class="flex flex-wrap items-center justify-center gap-3 md:gap-4">
|
<h1>{{ t("Category list") }}</h1>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-center gap-3 md:gap-4"
|
||||||
|
v-if="promotedCategories.length > 0"
|
||||||
|
>
|
||||||
<CategoryCard
|
<CategoryCard
|
||||||
v-for="category in promotedCategories"
|
v-for="category in promotedCategories"
|
||||||
:key="category.key"
|
:key="category.key"
|
||||||
|
@ -9,40 +13,31 @@
|
||||||
:with-details="true"
|
:with-details="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<EmptyContent icon="image" :inline="true">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"No categories with public upcoming events on this instance were found."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</EmptyContent>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="mx-auto w-full max-w-lg rounded-2xl dark:bg-gray-800 p-2 mt-10"
|
class="mx-auto w-full max-w-lg rounded-2xl dark:bg-gray-800 p-2 mt-10"
|
||||||
>
|
>
|
||||||
<div
|
<o-collapse
|
||||||
class="card"
|
v-model:open="isLicencePanelOpen"
|
||||||
animation="slide"
|
|
||||||
:open="isLicencePanelOpen"
|
|
||||||
@open="isLicencePanelOpen = !isLicencePanelOpen"
|
|
||||||
:aria-id="'contentIdForA11y5'"
|
:aria-id="'contentIdForA11y5'"
|
||||||
>
|
>
|
||||||
<div>
|
<template #trigger>
|
||||||
<button
|
<o-button
|
||||||
class="flex w-full justify-between rounded-lg px-4 py-2 text-left text-sm font-medium dark:text-zinc-300"
|
aria-controls="contentIdForA11y1"
|
||||||
|
:icon-right="isLicencePanelOpen ? 'chevron-up' : 'chevron-down'"
|
||||||
>
|
>
|
||||||
{{ t("Category illustrations credits") }}
|
{{ t("Category illustrations credits") }}
|
||||||
<svg
|
</o-button>
|
||||||
width="24"
|
</template>
|
||||||
height="24"
|
|
||||||
:class="isLicencePanelOpen ? 'transform rotate-90' : ''"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="{2}"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col dark:text-zinc-300 gap-2 py-4 px-1">
|
<div class="flex flex-col dark:text-zinc-300 gap-2 py-4 px-1">
|
||||||
<p
|
<p
|
||||||
v-for="(categoryLicence, key) in categoriesPicturesLicences"
|
v-for="(categoryLicence, key) in categoriesPicturesLicences"
|
||||||
|
@ -88,7 +83,7 @@
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</o-collapse>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,6 +102,7 @@ import {
|
||||||
} from "@/components/Categories/constants";
|
} from "@/components/Categories/constants";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useEventCategories } from "@/composition/apollo/config";
|
import { useEventCategories } from "@/composition/apollo/config";
|
||||||
|
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1674,6 +1674,7 @@ defmodule Mobilizon.Events do
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec category_statistics :: [{String.t(), non_neg_integer()}]
|
||||||
def category_statistics do
|
def category_statistics do
|
||||||
Event
|
Event
|
||||||
|> filter_future_events(true)
|
|> filter_future_events(true)
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png" sizes="152x152" />
|
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png" sizes="152x152" />
|
||||||
<link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()} />
|
<link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()} />
|
||||||
<meta name="theme-color" content={theme_color()} />
|
<meta name="theme-color" content={theme_color()} />
|
||||||
|
<%= if is_root(assigns) do %>
|
||||||
<link rel="preload" href="/img/shape-1.svg" as="image" />
|
<link rel="preload" href="/img/shape-1.svg" as="image" />
|
||||||
<link rel="preload" href="/img/shape-2.svg" as="image" />
|
<link rel="preload" href="/img/shape-2.svg" as="image" />
|
||||||
<link rel="preload" href="/img/shape-3.svg" as="image" />
|
<link rel="preload" href="/img/shape-3.svg" as="image" />
|
||||||
|
<% end %>
|
||||||
<%= tags(assigns) || assigns.tags %>
|
<%= tags(assigns) || assigns.tags %>
|
||||||
<%= Vite.inlined_phx_manifest() %>
|
<%= Vite.inlined_phx_manifest() %>
|
||||||
<%= Vite.vite_client() %>
|
<%= Vite.vite_client() %>
|
||||||
|
|
|
@ -86,4 +86,9 @@ defmodule Mobilizon.Web.PageView do
|
||||||
def language_direction(assigns) do
|
def language_direction(assigns) do
|
||||||
assigns |> Map.get(:locale, "en") |> get_language_direction()
|
assigns |> Map.get(:locale, "en") |> get_language_direction()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec is_root(map()) :: boolean()
|
||||||
|
def is_root(assigns) do
|
||||||
|
assigns |> Map.get(:conn, %{request_path: "/"}) |> Map.get(:request_path, "/") == "/"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue