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>
|
||||
<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=""
|
||||
|
@ -55,7 +55,7 @@
|
||||
import { LocationType } from "../../types/user-location.model";
|
||||
import MoreContent from "./MoreContent.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 { IEvent } from "@/types/event.model";
|
||||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
|
@ -13,7 +13,7 @@
|
||||
>
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
:class="{ 'is-active': editor?.isActive('bold') }"
|
||||
@click="editor?.chain().focus().toggleBold().run()"
|
||||
type="button"
|
||||
:title="t('Bold')"
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
:class="{ 'is-active': editor?.isActive('italic') }"
|
||||
@click="editor?.chain().focus().toggleItalic().run()"
|
||||
type="button"
|
||||
:title="t('Italic')"
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
:class="{ 'is-active': editor?.isActive('underline') }"
|
||||
@click="editor?.chain().focus().toggleUnderline().run()"
|
||||
type="button"
|
||||
:title="t('Underline')"
|
||||
@ -44,7 +44,7 @@
|
||||
<button
|
||||
v-if="!isBasicMode"
|
||||
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()"
|
||||
type="button"
|
||||
:title="t('Heading Level 1')"
|
||||
@ -55,7 +55,7 @@
|
||||
<button
|
||||
v-if="!isBasicMode"
|
||||
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()"
|
||||
type="button"
|
||||
:title="t('Heading Level 2')"
|
||||
@ -66,7 +66,7 @@
|
||||
<button
|
||||
v-if="!isBasicMode"
|
||||
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()"
|
||||
type="button"
|
||||
:title="t('Heading Level 3')"
|
||||
@ -77,7 +77,7 @@
|
||||
<button
|
||||
class="menubar__button"
|
||||
@click="showLinkMenu()"
|
||||
:class="{ 'is-active': editor.isActive('link') }"
|
||||
:class="{ 'is-active': editor?.isActive('link') }"
|
||||
type="button"
|
||||
:title="t('Add link')"
|
||||
>
|
||||
@ -85,7 +85,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="editor.isActive('link')"
|
||||
v-if="editor?.isActive('link')"
|
||||
class="menubar__button"
|
||||
@click="editor?.chain().focus().unsetLink().run()"
|
||||
type="button"
|
||||
@ -107,7 +107,7 @@
|
||||
<button
|
||||
class="menubar__button"
|
||||
v-if="!isBasicMode"
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
:class="{ 'is-active': editor?.isActive('bulletList') }"
|
||||
@click="editor?.chain().focus().toggleBulletList().run()"
|
||||
type="button"
|
||||
:title="t('Bullet list')"
|
||||
@ -118,7 +118,7 @@
|
||||
<button
|
||||
v-if="!isBasicMode"
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||
:class="{ 'is-active': editor?.isActive('orderedList') }"
|
||||
@click="editor?.chain().focus().toggleOrderedList().run()"
|
||||
type="button"
|
||||
:title="t('Ordered list')"
|
||||
@ -129,7 +129,7 @@
|
||||
<button
|
||||
v-if="!isBasicMode"
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||
:class="{ 'is-active': editor?.isActive('blockquote') }"
|
||||
@click="editor?.chain().focus().toggleBlockquote().run()"
|
||||
type="button"
|
||||
:title="t('Quote')"
|
||||
@ -193,7 +193,7 @@
|
||||
</template>
|
||||
|
||||
<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 BulletList from "@tiptap/extension-bullet-list";
|
||||
import Heading from "@tiptap/extension-heading";
|
||||
@ -218,7 +218,7 @@ import Underline from "@tiptap/extension-underline";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import { AutoDir } from "./Editor/Autodir";
|
||||
// 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 { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
@ -254,8 +254,6 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const editor = ref<Editor | null>(null);
|
||||
|
||||
const isDescriptionMode = computed((): boolean => {
|
||||
return props.mode === "description" || isBasicMode.value;
|
||||
});
|
||||
@ -278,13 +276,28 @@ const isBasicMode = computed((): boolean => {
|
||||
|
||||
// const observer = ref<MutationObserver | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
editor.value = new Editor({
|
||||
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 editor = useEditor({
|
||||
editorProps: {
|
||||
attributes: {
|
||||
"aria-multiline": isShortMode.value.toString(),
|
||||
"aria-label": props.ariaLabel ?? "",
|
||||
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,
|
||||
},
|
||||
@ -317,21 +330,6 @@ onMounted(() => {
|
||||
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);
|
||||
|
||||
@ -351,6 +349,7 @@ const { t } = useI18n({ useScope: "global" });
|
||||
const showLinkMenu = (): void => {
|
||||
dialog?.prompt({
|
||||
message: t("Enter the link URL"),
|
||||
hasInput: true,
|
||||
inputAttrs: {
|
||||
type: "url",
|
||||
},
|
||||
|
@ -28,7 +28,7 @@ export const VALIDATE_USER = gql`
|
||||
`;
|
||||
|
||||
export const LOGGED_USER = gql`
|
||||
query {
|
||||
query LoggedUserQuery {
|
||||
loggedUser {
|
||||
id
|
||||
email
|
||||
@ -300,7 +300,7 @@ export const UPDATE_USER_LOCALE = gql`
|
||||
`;
|
||||
|
||||
export const FEED_TOKENS_LOGGED_USER = gql`
|
||||
query {
|
||||
query FeedTokensLoggedUser {
|
||||
loggedUser {
|
||||
id
|
||||
feedTokens {
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="container mx-auto py-4 md:py-12 px-2 md:px-60">
|
||||
<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
|
||||
v-for="category in promotedCategories"
|
||||
:key="category.key"
|
||||
@ -9,40 +13,31 @@
|
||||
:with-details="true"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<EmptyContent icon="image" :inline="true">
|
||||
{{
|
||||
t(
|
||||
"No categories with public upcoming events on this instance were found."
|
||||
)
|
||||
}}
|
||||
</EmptyContent>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mx-auto w-full max-w-lg rounded-2xl dark:bg-gray-800 p-2 mt-10"
|
||||
>
|
||||
<div
|
||||
class="card"
|
||||
animation="slide"
|
||||
:open="isLicencePanelOpen"
|
||||
@open="isLicencePanelOpen = !isLicencePanelOpen"
|
||||
<o-collapse
|
||||
v-model:open="isLicencePanelOpen"
|
||||
:aria-id="'contentIdForA11y5'"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="flex w-full justify-between rounded-lg px-4 py-2 text-left text-sm font-medium dark:text-zinc-300"
|
||||
<template #trigger>
|
||||
<o-button
|
||||
aria-controls="contentIdForA11y1"
|
||||
:icon-right="isLicencePanelOpen ? 'chevron-up' : 'chevron-down'"
|
||||
>
|
||||
{{ t("Category illustrations credits") }}
|
||||
<svg
|
||||
width="24"
|
||||
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>
|
||||
</o-button>
|
||||
</template>
|
||||
<div class="flex flex-col dark:text-zinc-300 gap-2 py-4 px-1">
|
||||
<p
|
||||
v-for="(categoryLicence, key) in categoriesPicturesLicences"
|
||||
@ -88,7 +83,7 @@
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</o-collapse>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@ -107,6 +102,7 @@ import {
|
||||
} from "@/components/Categories/constants";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useEventCategories } from "@/composition/apollo/config";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
|
||||
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()
|
||||
end
|
||||
|
||||
@spec category_statistics :: [{String.t(), non_neg_integer()}]
|
||||
def category_statistics do
|
||||
Event
|
||||
|> 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="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={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-2.svg" as="image" />
|
||||
<link rel="preload" href="/img/shape-3.svg" as="image" />
|
||||
<% end %>
|
||||
<%= tags(assigns) || assigns.tags %>
|
||||
<%= Vite.inlined_phx_manifest() %>
|
||||
<%= Vite.vite_client() %>
|
||||
|
@ -86,4 +86,9 @@ defmodule Mobilizon.Web.PageView do
|
||||
def language_direction(assigns) do
|
||||
assigns |> Map.get(:locale, "en") |> get_language_direction()
|
||||
end
|
||||
|
||||
@spec is_root(map()) :: boolean()
|
||||
def is_root(assigns) do
|
||||
assigns |> Map.get(:conn, %{request_path: "/"}) |> Map.get(:request_path, "/") == "/"
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user