build: switch from yarn to npm to manage js dependencies and move js contents to root
yarn v1 is being deprecated and starts to have some issues Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
116
src/components/Participation/ConfirmParticipation.vue
Normal file
116
src/components/Participation/ConfirmParticipation.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<section class="container mx-auto">
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ t("Your participation request is being validated") }}
|
||||
</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed && participation === undefined">
|
||||
<o-notification
|
||||
:title="t('Error while validating participation request')"
|
||||
variant="danger"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
"Either the participation request has already been validated, either the validation token is incorrect."
|
||||
)
|
||||
}}
|
||||
</o-notification>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">
|
||||
{{ t("Your participation request has been validated") }}
|
||||
</h1>
|
||||
<p
|
||||
class="prose dark:prose-invert"
|
||||
v-if="participation?.event.joinOptions == EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
{{
|
||||
t("Your participation still has to be approved by the organisers.")
|
||||
}}
|
||||
</p>
|
||||
<div v-if="failed">
|
||||
<o-notification
|
||||
:title="
|
||||
t('Error while updating participation status inside this browser')
|
||||
"
|
||||
variant="warning"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
"We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue."
|
||||
)
|
||||
}}
|
||||
</o-notification>
|
||||
</div>
|
||||
<div class="columns has-text-centered">
|
||||
<div class="column">
|
||||
<o-button
|
||||
tag="router-link"
|
||||
variant="primary"
|
||||
size="large"
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: participation?.event.uuid },
|
||||
}"
|
||||
>{{ t("Go to the event page") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
|
||||
import { EventJoinOptions } from "@/types/enums";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Confirm participation")),
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
}>();
|
||||
|
||||
const loading = ref(true);
|
||||
const failed = ref(false);
|
||||
const participation = ref<IParticipant | null | undefined>(null);
|
||||
|
||||
const { onDone, onError, mutate } = useMutation<{
|
||||
confirmParticipation: IParticipant;
|
||||
}>(CONFIRM_PARTICIPATION);
|
||||
|
||||
const participationToken = computed(() => props.token);
|
||||
|
||||
watchEffect(() => {
|
||||
if (participationToken.value) {
|
||||
mutate({
|
||||
token: participationToken.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDone(async ({ data }) => {
|
||||
participation.value = data?.confirmParticipation;
|
||||
if (participation.value) {
|
||||
await confirmLocalAnonymousParticipation(participation.value?.event.uuid);
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
console.error(err);
|
||||
failed.value = true;
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
109
src/components/Participation/NewPrivateMessage.vue
Normal file
109
src/components/Participation/NewPrivateMessage.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<form @submit="sendForm">
|
||||
<Editor
|
||||
v-model="text"
|
||||
mode="basic"
|
||||
:aria-label="t('Message body')"
|
||||
v-if="currentActor"
|
||||
:currentActor="currentActor"
|
||||
:placeholder="t('Write a new message')"
|
||||
/>
|
||||
<o-button class="mt-3" nativeType="submit">{{ t("Send") }}</o-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { SEND_EVENT_PRIVATE_MESSAGE_MUTATION } from "@/graphql/conversations";
|
||||
import { EVENT_CONVERSATIONS } from "@/graphql/event";
|
||||
import { IConversation } from "@/types/conversation";
|
||||
import { ParticipantRole } from "@/types/enums";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const props = defineProps<{
|
||||
event: IEvent;
|
||||
}>();
|
||||
|
||||
const event = computed(() => props.event);
|
||||
|
||||
const text = ref("");
|
||||
const {
|
||||
mutate: eventPrivateMessageMutate,
|
||||
onDone: onEventPrivateMessageMutated,
|
||||
} = useMutation<
|
||||
{
|
||||
sendEventPrivateMessage: IConversation;
|
||||
},
|
||||
{
|
||||
text: string;
|
||||
actorId: string;
|
||||
eventId: string;
|
||||
roles?: string;
|
||||
inReplyToActorId?: ParticipantRole[];
|
||||
language?: string;
|
||||
}
|
||||
>(SEND_EVENT_PRIVATE_MESSAGE_MUTATION, {
|
||||
update(cache, result) {
|
||||
if (!result.data?.sendEventPrivateMessage) return;
|
||||
const cachedData = cache.readQuery<{
|
||||
event: Pick<IEvent, "conversations" | "id" | "uuid">;
|
||||
}>({
|
||||
query: EVENT_CONVERSATIONS,
|
||||
variables: {
|
||||
uuid: event.value.uuid,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
if (!cachedData) return;
|
||||
cache.writeQuery({
|
||||
query: EVENT_CONVERSATIONS,
|
||||
variables: {
|
||||
uuid: event.value.uuid,
|
||||
page: 1,
|
||||
},
|
||||
data: {
|
||||
event: {
|
||||
...cachedData?.event,
|
||||
conversations: {
|
||||
...cachedData.event.conversations,
|
||||
total: cachedData.event.conversations.total + 1,
|
||||
elements: [
|
||||
...cachedData.event.conversations.elements,
|
||||
result.data.sendEventPrivateMessage,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const sendForm = (e: Event) => {
|
||||
e.preventDefault();
|
||||
console.debug("Sending new private message");
|
||||
if (!currentActor.value?.id || !event.value.id) return;
|
||||
eventPrivateMessageMutate({
|
||||
text: text.value,
|
||||
actorId:
|
||||
event.value?.attributedTo?.id ??
|
||||
event.value.organizerActor?.id ??
|
||||
currentActor.value?.id,
|
||||
eventId: event.value.id,
|
||||
});
|
||||
};
|
||||
|
||||
onEventPrivateMessageMutated(() => {
|
||||
text.value = "";
|
||||
});
|
||||
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("../../components/TextEditor.vue")
|
||||
);
|
||||
</script>
|
||||
218
src/components/Participation/ParticipationSection.vue
Normal file
218
src/components/Participation/ParticipationSection.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="event-participation" v-if="isEventNotAlreadyPassed">
|
||||
<participation-button
|
||||
v-if="shouldShowParticipationButton"
|
||||
:participation="participation"
|
||||
:event="event"
|
||||
:current-actor="currentActor"
|
||||
:identities="identities"
|
||||
@join-event="(actor) => $emit('join-event', actor)"
|
||||
@join-modal="$emit('join-modal')"
|
||||
@join-event-with-confirmation="
|
||||
(actor) => $emit('join-event-with-confirmation', actor)
|
||||
"
|
||||
@confirm-leave="$emit('confirm-leave')"
|
||||
/>
|
||||
<o-button
|
||||
variant="text"
|
||||
v-if="!actorIsParticipant && anonymousParticipation !== null"
|
||||
@click="$emit('cancel-anonymous-participation')"
|
||||
>{{ t("Cancel anonymous participation") }}</o-button
|
||||
>
|
||||
<small v-if="!actorIsParticipant && anonymousParticipation">
|
||||
{{ t("You are participating in this event anonymously") }}
|
||||
<VTooltip>
|
||||
<template #popper>
|
||||
{{ t("Click for more information") }}
|
||||
</template>
|
||||
<span @click="isAnonymousParticipationModalOpen = true">
|
||||
<InformationOutline :size="16" />
|
||||
</span>
|
||||
</VTooltip>
|
||||
</small>
|
||||
<small
|
||||
v-else-if="!actorIsParticipant && anonymousParticipation === false"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
"You are participating in this event anonymously but didn't confirm participation"
|
||||
)
|
||||
}}
|
||||
<VTooltip>
|
||||
<template #popper>
|
||||
{{
|
||||
t(
|
||||
"This information is saved only on your computer. Click for details"
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
<router-link :to="{ name: RouteName.TERMS }">
|
||||
<HelpCircleOutline :size="16" />
|
||||
</router-link>
|
||||
</VTooltip>
|
||||
</small>
|
||||
</div>
|
||||
<div v-else>
|
||||
<o-button variant="primary" disabled icon-left="menu-down">
|
||||
{{ t("Event already passed") }}
|
||||
</o-button>
|
||||
</div>
|
||||
<o-modal
|
||||
v-model:active="isAnonymousParticipationModalOpen"
|
||||
has-modal-card
|
||||
:close-button-aria-label="t('Close')"
|
||||
ref="anonymous-participation-modal"
|
||||
>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">
|
||||
{{ t("About anonymous participation") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<o-notification
|
||||
variant="primary"
|
||||
:closable="false"
|
||||
v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
"As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted."
|
||||
)
|
||||
}}
|
||||
</o-notification>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
"Your participation status is saved only on this device and will be deleted one month after the event's passed."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p v-if="isSecureContext()">
|
||||
{{
|
||||
t(
|
||||
"You may clear all participation information for this device with the buttons below."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="buttons" v-if="isSecureContext()">
|
||||
<o-button
|
||||
variant="danger"
|
||||
outlined
|
||||
@click="clearEventParticipationData"
|
||||
>
|
||||
{{ t("Clear participation data for this event") }}
|
||||
</o-button>
|
||||
<o-button variant="danger" @click="clearAllParticipationData">
|
||||
{{ t("Clear participation data for all events") }}
|
||||
</o-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</o-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { EventJoinOptions, EventStatus, ParticipantRole } from "@/types/enums";
|
||||
import { IParticipant } from "@/types/participant.model";
|
||||
import RouteName from "@/router/name";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import {
|
||||
removeAllAnonymousParticipations,
|
||||
removeAnonymousParticipation,
|
||||
} from "@/services/AnonymousParticipationStorage";
|
||||
import ParticipationButton from "../Event/ParticipationButton.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import InformationOutline from "vue-material-design-icons/InformationOutline.vue";
|
||||
import HelpCircleOutline from "vue-material-design-icons/HelpCircleOutline.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { IAnonymousParticipationConfig } from "@/types/config.model";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
participation: IParticipant | undefined;
|
||||
event: IEvent;
|
||||
anonymousParticipation?: boolean | null;
|
||||
currentActor: IPerson | undefined;
|
||||
identities: IPerson[] | undefined;
|
||||
anonymousParticipationConfig: IAnonymousParticipationConfig;
|
||||
}>(),
|
||||
{
|
||||
anonymousParticipation: null,
|
||||
}
|
||||
);
|
||||
|
||||
const isAnonymousParticipationModalOpen = ref(false);
|
||||
|
||||
const actorIsParticipant = computed((): boolean => {
|
||||
if (actorIsOrganizer.value) return true;
|
||||
|
||||
return props.participation?.role === ParticipantRole.PARTICIPANT;
|
||||
});
|
||||
|
||||
const actorIsOrganizer = computed((): boolean => {
|
||||
return props.participation?.role === ParticipantRole.CREATOR;
|
||||
});
|
||||
|
||||
const shouldShowParticipationButton = computed((): boolean => {
|
||||
// If we have an anonymous participation, don't show the participation button
|
||||
if (
|
||||
props.anonymousParticipationConfig?.allowed &&
|
||||
props.anonymousParticipation
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// So that people can cancel their participation
|
||||
if (actorIsParticipant.value) return true;
|
||||
|
||||
// You can participate to draft or cancelled events
|
||||
if (props.event.draft || props.event.status === EventStatus.CANCELLED)
|
||||
return false;
|
||||
|
||||
// If capacity is OK
|
||||
if (eventCapacityOK.value) return true;
|
||||
|
||||
// Else
|
||||
return false;
|
||||
});
|
||||
|
||||
const eventCapacityOK = computed((): boolean => {
|
||||
if (props.event.draft) return true;
|
||||
if (!props.event.options.maximumAttendeeCapacity) return true;
|
||||
return (
|
||||
props.event.options.maximumAttendeeCapacity >
|
||||
props.event.participantStats.participant
|
||||
);
|
||||
});
|
||||
|
||||
const isEventNotAlreadyPassed = computed((): boolean => {
|
||||
return new Date(endDate.value) > new Date();
|
||||
});
|
||||
|
||||
const endDate = computed((): string => {
|
||||
return props.event.endsOn !== null &&
|
||||
props.event.endsOn > props.event.beginsOn
|
||||
? props.event.endsOn
|
||||
: props.event.beginsOn;
|
||||
});
|
||||
|
||||
const isSecureContext = (): boolean => {
|
||||
return window.isSecureContext;
|
||||
};
|
||||
|
||||
const clearEventParticipationData = async (): Promise<void> => {
|
||||
await removeAnonymousParticipation(props.event.uuid);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const clearAllParticipationData = (): void => {
|
||||
removeAllAnonymousParticipations();
|
||||
window.location.reload();
|
||||
};
|
||||
</script>
|
||||
36
src/components/Participation/ParticipationWithAccount.vue
Normal file
36
src/components/Participation/ParticipationWithAccount.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<redirect-with-account
|
||||
v-if="uri"
|
||||
:uri="uri"
|
||||
:pathAfterLogin="`/events/${uuid}`"
|
||||
:sentence="sentence"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
|
||||
import { useFetchEvent } from "@/composition/apollo/event";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
uuid: string;
|
||||
}>();
|
||||
|
||||
const { event } = useFetchEvent(computed(() => props.uuid));
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Participation with account")),
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
});
|
||||
|
||||
const uri = computed((): string | undefined => {
|
||||
return event.value?.url;
|
||||
});
|
||||
|
||||
const sentence = t(
|
||||
"We will redirect you to your instance in order to interact with this event"
|
||||
);
|
||||
</script>
|
||||
277
src/components/Participation/ParticipationWithoutAccount.vue
Normal file
277
src/components/Participation/ParticipationWithoutAccount.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<section class="container mx-auto">
|
||||
<div class="" v-if="event">
|
||||
<form @submit.prevent="joinEvent" v-if="!formSent">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<o-notification variant="info">
|
||||
{{
|
||||
$t(
|
||||
"Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer."
|
||||
)
|
||||
}}
|
||||
</o-notification>
|
||||
<o-notification variant="danger" v-if="error">{{
|
||||
error
|
||||
}}</o-notification>
|
||||
<o-field
|
||||
:label="$t('Email address')"
|
||||
labelFor="anonymousParticipationEmail"
|
||||
>
|
||||
<o-input
|
||||
type="email"
|
||||
id="anonymousParticipationEmail"
|
||||
v-model="anonymousParticipation.email"
|
||||
:placeholder="$t('Your email')"
|
||||
required
|
||||
/>
|
||||
</o-field>
|
||||
<p v-if="event.joinOptions === EventJoinOptions.RESTRICTED">
|
||||
{{
|
||||
$t(
|
||||
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{
|
||||
$t(
|
||||
"If you want, you may send a message to the event organizer here."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<o-field
|
||||
:label="$t('Message')"
|
||||
labelFor="anonymousParticipationMessage"
|
||||
>
|
||||
<o-input
|
||||
id="anonymousParticipationMessage"
|
||||
type="textarea"
|
||||
v-model="anonymousParticipation.message"
|
||||
minlength="10"
|
||||
:required="event.joinOptions === EventJoinOptions.RESTRICTED"
|
||||
/>
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-checkbox v-model="anonymousParticipation.saveParticipation">
|
||||
<b>{{ $t("Remember my participation in this browser") }}</b>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</o-checkbox>
|
||||
</o-field>
|
||||
<div class="flex gap-2 my-2">
|
||||
<o-button
|
||||
:disabled="sendingForm"
|
||||
variant="primary"
|
||||
native-type="submit"
|
||||
>{{ $t("Send email") }}</o-button
|
||||
>
|
||||
<o-button
|
||||
native-type="button"
|
||||
variant="text"
|
||||
@click="$router.go(-1)"
|
||||
>{{ $t("Back to previous page") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<h1 class="title">
|
||||
{{ $t("Request for participation confirmation sent") }}
|
||||
</h1>
|
||||
<p class="prose dark:prose-invert">
|
||||
<span>{{ $t("Check your inbox (and your junk mail folder).") }}</span>
|
||||
<span
|
||||
class="details"
|
||||
v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation."
|
||||
)
|
||||
}} </span
|
||||
><span class="details" v-else>{{
|
||||
$t(
|
||||
"Your participation will be validated once you click the confirmation link into the email."
|
||||
)
|
||||
}}</span>
|
||||
</p>
|
||||
<o-notification variant="warning" v-if="error">{{
|
||||
error
|
||||
}}</o-notification>
|
||||
<p class="prose dark:prose-invert">
|
||||
<i18n-t
|
||||
keypath="You may now close this window, or {return_to_event}."
|
||||
>
|
||||
<template #return_to_event>
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||
>{{ $t("return to the event's page") }}</router-link
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<o-notification variant="danger" v-else-if="!loading"
|
||||
>{{
|
||||
$t(
|
||||
"Unable to load event for participation. The error details are provided below:"
|
||||
)
|
||||
}}
|
||||
<details>
|
||||
<pre>{{ error }}</pre>
|
||||
</details>
|
||||
</o-notification>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
|
||||
import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
|
||||
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
|
||||
import RouteName from "@/router/name";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { useFetchEventBasic } from "@/composition/apollo/event";
|
||||
import { useAnonymousActorId } from "@/composition/apollo/config";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
|
||||
const error = ref<boolean | string>(false);
|
||||
|
||||
const { anonymousActorId } = useAnonymousActorId();
|
||||
|
||||
const props = defineProps<{
|
||||
uuid: string;
|
||||
}>();
|
||||
const { event, loading } = useFetchEventBasic(computed(() => props.uuid));
|
||||
|
||||
const { t, locale } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Participation without account")),
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
});
|
||||
|
||||
const anonymousParticipation = reactive<{
|
||||
email: string;
|
||||
message: string;
|
||||
saveParticipation: boolean;
|
||||
}>({
|
||||
email: "",
|
||||
message: "",
|
||||
saveParticipation: true,
|
||||
});
|
||||
|
||||
const formSent = ref(false);
|
||||
|
||||
const sendingForm = ref(false);
|
||||
|
||||
const {
|
||||
mutate: joinEventMutation,
|
||||
onDone: joinEventDone,
|
||||
onError: joinEventError,
|
||||
} = useMutation<{
|
||||
joinEvent: IParticipant;
|
||||
}>(JOIN_EVENT, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ joinEvent: IParticipant }>,
|
||||
{ data: updateData }: FetchResult
|
||||
) => {
|
||||
if (updateData == null) {
|
||||
console.error(
|
||||
"Cannot update event participant cache, because of data null value."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedData = store.readQuery<{ event: IEvent }>({
|
||||
query: FETCH_EVENT_BASIC,
|
||||
variables: { uuid: event.value?.uuid },
|
||||
});
|
||||
if (cachedData == null) {
|
||||
console.error(
|
||||
"Cannot update event participant cache, because of cached null value."
|
||||
);
|
||||
return;
|
||||
}
|
||||
const participantStats = { ...cachedData.event.participantStats };
|
||||
|
||||
if (updateData.joinEvent.role === ParticipantRole.NOT_CONFIRMED) {
|
||||
participantStats.notConfirmed += 1;
|
||||
} else {
|
||||
participantStats.going += 1;
|
||||
participantStats.participant += 1;
|
||||
}
|
||||
|
||||
store.writeQuery({
|
||||
query: FETCH_EVENT_BASIC,
|
||||
variables: { uuid: event.value?.uuid },
|
||||
data: {
|
||||
event: {
|
||||
...cachedData.event,
|
||||
participantStats,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
joinEventDone(async ({ data }) => {
|
||||
sendingForm.value = false;
|
||||
formSent.value = true;
|
||||
if (
|
||||
data?.joinEvent.metadata.cancellationToken &&
|
||||
anonymousParticipation.saveParticipation &&
|
||||
event.value
|
||||
) {
|
||||
try {
|
||||
await addLocalUnconfirmedAnonymousParticipation(
|
||||
event.value,
|
||||
data.joinEvent.metadata.cancellationToken
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (
|
||||
["TextEncoder is not defined", "crypto.subtle is undefined"].includes(
|
||||
e.message
|
||||
)
|
||||
) {
|
||||
error.value = t("Unable to save your participation in this browser.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
joinEventError((e) => {
|
||||
if (e.graphQLErrors && e.graphQLErrors.length > 0) {
|
||||
error.value = e.graphQLErrors[0].message;
|
||||
} else if (e.networkError) {
|
||||
error.value = e.networkError.message;
|
||||
}
|
||||
});
|
||||
|
||||
const joinEvent = async (): Promise<void> => {
|
||||
error.value = false;
|
||||
sendingForm.value = true;
|
||||
joinEventMutation({
|
||||
eventId: event.value?.id,
|
||||
actorId: anonymousActorId.value,
|
||||
email: anonymousParticipation.email,
|
||||
message: anonymousParticipation.message,
|
||||
locale: locale,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
143
src/components/Participation/UnloggedParticipation.vue
Normal file
143
src/components/Participation/UnloggedParticipation.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<section class="container mx-auto max-w-2xl">
|
||||
<h2 class="text-2xl">
|
||||
{{ t("You wish to participate to the following event") }}
|
||||
</h2>
|
||||
<EventListViewCard v-if="event" :event="event" />
|
||||
<div class="flex flex-wrap gap-4 items-center w-full my-6">
|
||||
<div class="bg-white dark:bg-zinc-700 rounded-md p-4 flex-1">
|
||||
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
|
||||
<figure class="flex justify-center my-2">
|
||||
<img
|
||||
src="../../../public/img/undraw_profile.svg"
|
||||
alt="Profile illustration"
|
||||
width="128"
|
||||
height="128"
|
||||
/>
|
||||
</figure>
|
||||
<o-button variant="primary">{{
|
||||
t("I have a Mobilizon account")
|
||||
}}</o-button>
|
||||
</router-link>
|
||||
<p>
|
||||
<small>
|
||||
{{
|
||||
t("Either on the {instance} instance or on another instance.", {
|
||||
instance: host,
|
||||
})
|
||||
}}
|
||||
</small>
|
||||
<o-tooltip
|
||||
variant="dark"
|
||||
:label="
|
||||
t(
|
||||
'Mobilizon is a federated network. You can interact with this event from a different server.'
|
||||
)
|
||||
"
|
||||
>
|
||||
<o-icon size="small" icon="help-circle-outline" />
|
||||
</o-tooltip>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white dark:bg-zinc-700 rounded-md p-4 flex-1"
|
||||
v-if="
|
||||
event &&
|
||||
anonymousParticipationAllowed &&
|
||||
hasAnonymousEmailParticipationMethod
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
|
||||
v-if="event.local"
|
||||
>
|
||||
<figure class="flex justify-center my-2">
|
||||
<img
|
||||
width="128"
|
||||
height="128"
|
||||
src="../../../public/img/undraw_mail_2.svg"
|
||||
alt="Privacy illustration"
|
||||
/>
|
||||
</figure>
|
||||
<o-button variant="primary">{{
|
||||
t("I don't have a Mobilizon account")
|
||||
}}</o-button>
|
||||
</router-link>
|
||||
<a :href="`${event.url}/participate/without-account`" v-else>
|
||||
<figure class="flex justify-center my-2">
|
||||
<img
|
||||
src="../../../public/img/undraw_mail_2.svg"
|
||||
width="128"
|
||||
height="128"
|
||||
alt="Privacy illustration"
|
||||
/>
|
||||
</figure>
|
||||
<o-button variant="primary">{{
|
||||
t("I don't have a Mobilizon account")
|
||||
}}</o-button>
|
||||
</a>
|
||||
<p>
|
||||
<small>{{ t("Participate using your email address") }}</small>
|
||||
<br />
|
||||
<small v-if="!event.local">
|
||||
{{ t("You will be redirected to the original instance") }}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-text-centered">
|
||||
<o-button tag="a" variant="text" @click="router.go(-1)">{{
|
||||
t("Back to previous page")
|
||||
}}</o-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import EventListViewCard from "@/components/Event/EventListViewCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import { useFetchEvent } from "@/composition/apollo/event";
|
||||
import { useAnonymousParticipationConfig } from "@/composition/apollo/config";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ uuid: string }>();
|
||||
|
||||
const { event } = useFetchEvent(computed(() => props.uuid));
|
||||
|
||||
const { anonymousParticipationConfig } = useAnonymousParticipationConfig();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Unlogged participation")),
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
});
|
||||
|
||||
const host = computed((): string => {
|
||||
return window.location.hostname;
|
||||
});
|
||||
|
||||
const anonymousParticipationAllowed = computed((): boolean | undefined => {
|
||||
return event.value?.options.anonymousParticipation;
|
||||
});
|
||||
|
||||
const hasAnonymousEmailParticipationMethod = computed(
|
||||
(): boolean | undefined => {
|
||||
return (
|
||||
anonymousParticipationConfig.value?.allowed &&
|
||||
anonymousParticipationConfig.value?.validation.email.enabled
|
||||
);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.column > a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user