Migrate to Vue 3 and Vite
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -1,48 +1,48 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<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">
|
||||
<b-message
|
||||
<o-notification
|
||||
:title="$t('Error while validating participation request')"
|
||||
type="is-danger"
|
||||
variant="danger"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"Either the participation request has already been validated, either the validation token is incorrect."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</o-notification>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">
|
||||
{{ $t("Your participation request has been validated") }}
|
||||
</h1>
|
||||
<p
|
||||
class="content"
|
||||
v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED"
|
||||
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">
|
||||
<b-message
|
||||
<o-notification
|
||||
:title="
|
||||
$t(
|
||||
'Error while updating participation status inside this browser'
|
||||
)
|
||||
"
|
||||
type="is-warning"
|
||||
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."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</o-notification>
|
||||
</div>
|
||||
<div class="columns has-text-centered">
|
||||
<div class="column">
|
||||
@@ -52,7 +52,7 @@
|
||||
class="button is-primary is-large"
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: this.participation.event.uuid },
|
||||
params: { uuid: participation?.event.uuid },
|
||||
}"
|
||||
>{{ $t("Go to the event page") }}</router-link
|
||||
>
|
||||
@@ -63,60 +63,50 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
<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 } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
@Component({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Confirm participation") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class ConfirmParticipation extends Vue {
|
||||
@Prop({ type: String, required: true }) token!: string;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
loading = true;
|
||||
useHead({
|
||||
title: computed(() => t("Confirm participation")),
|
||||
});
|
||||
|
||||
failed = false;
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
}>();
|
||||
|
||||
participation!: IParticipant;
|
||||
const loading = ref(true);
|
||||
const failed = ref(false);
|
||||
const participation = ref<IParticipant | null | undefined>(null);
|
||||
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
const { onDone, onError, mutate } = useMutation<{
|
||||
confirmParticipation: IParticipant;
|
||||
}>(CONFIRM_PARTICIPATION);
|
||||
|
||||
RouteName = RouteName;
|
||||
mutate({
|
||||
token: props.token,
|
||||
});
|
||||
|
||||
async created(): Promise<void> {
|
||||
await this.validateAction();
|
||||
onDone(async ({ data }) => {
|
||||
participation.value = data?.confirmParticipation;
|
||||
if (participation.value) {
|
||||
await confirmLocalAnonymousParticipation(participation.value?.event.uuid);
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
async validateAction(): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{
|
||||
confirmParticipation: IParticipant;
|
||||
}>({
|
||||
mutation: CONFIRM_PARTICIPATION,
|
||||
variables: {
|
||||
token: this.token,
|
||||
},
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const { confirmParticipation: participation } = data;
|
||||
this.participation = participation;
|
||||
await confirmLocalAnonymousParticipation(this.participation.event.uuid);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.failed = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
onError((err) => {
|
||||
console.error(err);
|
||||
failed.value = true;
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="event-participation has-text-right"
|
||||
v-if="isEventNotAlreadyPassed"
|
||||
>
|
||||
<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="
|
||||
@@ -16,223 +14,207 @@
|
||||
"
|
||||
@confirm-leave="$emit('confirm-leave')"
|
||||
/>
|
||||
<b-button
|
||||
type="is-text"
|
||||
<o-button
|
||||
variant="text"
|
||||
v-if="!actorIsParticipant && anonymousParticipation !== null"
|
||||
@click="$emit('cancel-anonymous-participation')"
|
||||
>{{ $t("Cancel anonymous participation") }}</b-button
|
||||
>{{ t("Cancel anonymous participation") }}</o-button
|
||||
>
|
||||
<small v-if="!actorIsParticipant && anonymousParticipation">
|
||||
{{ $t("You are participating in this event anonymously") }}
|
||||
<b-tooltip :label="$t('Click for more information')">
|
||||
{{ t("You are participating in this event anonymously") }}
|
||||
<VTooltip>
|
||||
<template #popper>
|
||||
{{ t("Click for more information") }}
|
||||
</template>
|
||||
<span
|
||||
class="is-clickable"
|
||||
@click="isAnonymousParticipationModalOpen = true"
|
||||
>
|
||||
<b-icon size="is-small" icon="information-outline" />
|
||||
<InformationOutline :size="16" />
|
||||
</span>
|
||||
</b-tooltip>
|
||||
</VTooltip>
|
||||
</small>
|
||||
<small
|
||||
v-else-if="!actorIsParticipant && anonymousParticipation === false"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"You are participating in this event anonymously but didn't confirm participation"
|
||||
)
|
||||
}}
|
||||
<b-tooltip
|
||||
:label="
|
||||
$t(
|
||||
'This information is saved only on your computer. Click for details'
|
||||
)
|
||||
"
|
||||
>
|
||||
<VTooltip>
|
||||
<template #popper>
|
||||
{{
|
||||
t(
|
||||
"This information is saved only on your computer. Click for details"
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
<router-link :to="{ name: RouteName.TERMS }">
|
||||
<b-icon size="is-small" icon="help-circle-outline" />
|
||||
<HelpCircleOutline :size="16" />
|
||||
</router-link>
|
||||
</b-tooltip>
|
||||
</VTooltip>
|
||||
</small>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button class="button is-primary" type="button" slot="trigger" disabled>
|
||||
<template>
|
||||
<span>{{ $t("Event already passed") }}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
<o-button variant="primary" disabled icon-left="menu-down">
|
||||
{{ t("Event already passed") }}
|
||||
</o-button>
|
||||
</div>
|
||||
<b-modal
|
||||
:active.sync="isAnonymousParticipationModalOpen"
|
||||
<o-modal
|
||||
v-model:active="isAnonymousParticipationModalOpen"
|
||||
has-modal-card
|
||||
:close-button-aria-label="$t('Close')"
|
||||
: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") }}
|
||||
{{ t("About anonymous participation") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<b-notification
|
||||
type="is-primary"
|
||||
<o-notification
|
||||
variant="primary"
|
||||
:closable="false"
|
||||
v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
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."
|
||||
)
|
||||
}}
|
||||
</b-notification>
|
||||
</o-notification>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
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">
|
||||
<p v-if="isSecureContext()">
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"You may clear all participation information for this device with the buttons below."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="buttons" v-if="isSecureContext">
|
||||
<b-button
|
||||
<div class="buttons" v-if="isSecureContext()">
|
||||
<o-button
|
||||
type="is-danger is-outlined"
|
||||
@click="clearEventParticipationData"
|
||||
>
|
||||
{{ $t("Clear participation data for this event") }}
|
||||
</b-button>
|
||||
<b-button type="is-danger" @click="clearAllParticipationData">
|
||||
{{ $t("Clear participation data for all events") }}
|
||||
</b-button>
|
||||
{{ 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>
|
||||
</b-modal>
|
||||
</o-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { EventJoinOptions, EventStatus, ParticipantRole } from "@/types/enums";
|
||||
import { IParticipant } from "@/types/participant.model";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import RouteName from "@/router/name";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
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";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
config: CONFIG,
|
||||
},
|
||||
components: {
|
||||
ParticipationButton,
|
||||
},
|
||||
})
|
||||
export default class ParticipationSection extends Vue {
|
||||
@Prop({ required: true }) participation!: IParticipant;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@Prop({ required: true }) event!: IEvent;
|
||||
|
||||
@Prop({ required: true, default: null }) anonymousParticipation!:
|
||||
| boolean
|
||||
| null;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
|
||||
isAnonymousParticipationModalOpen = false;
|
||||
|
||||
get actorIsParticipant(): boolean {
|
||||
if (this.actorIsOrganizer) return true;
|
||||
|
||||
return (
|
||||
this.participation &&
|
||||
this.participation.role === ParticipantRole.PARTICIPANT
|
||||
);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
participation: IParticipant | undefined;
|
||||
event: IEvent;
|
||||
anonymousParticipation?: boolean | null;
|
||||
currentActor: IPerson;
|
||||
identities: IPerson[];
|
||||
anonymousParticipationConfig: IAnonymousParticipationConfig;
|
||||
}>(),
|
||||
{
|
||||
anonymousParticipation: null,
|
||||
}
|
||||
);
|
||||
|
||||
get actorIsOrganizer(): boolean {
|
||||
return (
|
||||
this.participation && this.participation.role === ParticipantRole.CREATOR
|
||||
);
|
||||
}
|
||||
const isAnonymousParticipationModalOpen = ref(false);
|
||||
|
||||
get shouldShowParticipationButton(): boolean {
|
||||
// If we have an anonymous participation, don't show the participation button
|
||||
if (
|
||||
this.config &&
|
||||
this.config.anonymous.participation.allowed &&
|
||||
this.anonymousParticipation
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const actorIsParticipant = computed((): boolean => {
|
||||
if (actorIsOrganizer.value) return true;
|
||||
|
||||
// So that people can cancel their participation
|
||||
if (this.actorIsParticipant) return true;
|
||||
return props.participation?.role === ParticipantRole.PARTICIPANT;
|
||||
});
|
||||
|
||||
// You can participate to draft or cancelled events
|
||||
if (this.event.draft || this.event.status === EventStatus.CANCELLED)
|
||||
return false;
|
||||
const actorIsOrganizer = computed((): boolean => {
|
||||
return props.participation?.role === ParticipantRole.CREATOR;
|
||||
});
|
||||
|
||||
// If capacity is OK
|
||||
if (this.eventCapacityOK) return true;
|
||||
|
||||
// Else
|
||||
const shouldShowParticipationButton = computed((): boolean => {
|
||||
// If we have an anonymous participation, don't show the participation button
|
||||
if (
|
||||
props.anonymousParticipationConfig?.allowed &&
|
||||
props.anonymousParticipation
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get eventCapacityOK(): boolean {
|
||||
if (this.event.draft) return true;
|
||||
if (!this.event.options.maximumAttendeeCapacity) return true;
|
||||
return (
|
||||
this.event.options.maximumAttendeeCapacity >
|
||||
this.event.participantStats.participant
|
||||
);
|
||||
}
|
||||
// So that people can cancel their participation
|
||||
if (actorIsParticipant.value) return true;
|
||||
|
||||
get isEventNotAlreadyPassed(): boolean {
|
||||
return new Date(this.endDate) > new Date();
|
||||
}
|
||||
// You can participate to draft or cancelled events
|
||||
if (props.event.draft || props.event.status === EventStatus.CANCELLED)
|
||||
return false;
|
||||
|
||||
get endDate(): Date {
|
||||
return this.event.endsOn !== null && this.event.endsOn > this.event.beginsOn
|
||||
? this.event.endsOn
|
||||
: this.event.beginsOn;
|
||||
}
|
||||
// If capacity is OK
|
||||
if (eventCapacityOK.value) return true;
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get isSecureContext(): boolean {
|
||||
return window.isSecureContext;
|
||||
}
|
||||
// Else
|
||||
return false;
|
||||
});
|
||||
|
||||
async clearEventParticipationData(): Promise<void> {
|
||||
await removeAnonymousParticipation(this.event.uuid);
|
||||
window.location.reload();
|
||||
}
|
||||
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
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
clearAllParticipationData(): void {
|
||||
removeAllAnonymousParticipations();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
const isEventNotAlreadyPassed = computed((): boolean => {
|
||||
return new Date(endDate.value) > new Date();
|
||||
});
|
||||
|
||||
const endDate = computed((): Date => {
|
||||
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>
|
||||
|
||||
@@ -6,46 +6,31 @@
|
||||
:sentence="sentence"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
|
||||
import { FETCH_EVENT } from "@/graphql/event";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { useFetchEvent } from "@/composition/apollo/event";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@Component({
|
||||
components: { RedirectWithAccount },
|
||||
apollo: {
|
||||
event: {
|
||||
query: FETCH_EVENT,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.uuid;
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Participation with account") as string,
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class ParticipationWithAccount extends Vue {
|
||||
@Prop({ type: String, required: true }) uuid!: string;
|
||||
const props = defineProps<{
|
||||
uuid: string;
|
||||
}>();
|
||||
|
||||
event!: IEvent;
|
||||
const { event } = useFetchEvent(props.uuid);
|
||||
|
||||
get uri(): string | undefined {
|
||||
return this.event?.url;
|
||||
}
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
sentence = this.$t(
|
||||
"We will redirect you to your instance in order to interact with this event"
|
||||
);
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -1,118 +1,128 @@
|
||||
<template>
|
||||
<section class="container section hero is-fullheight">
|
||||
<div class="hero-body" v-if="event">
|
||||
<div class="container">
|
||||
<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>
|
||||
<b-message type="is-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."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
<b-message type="is-danger" v-if="error">{{ error }}</b-message>
|
||||
<b-field :label="$t('Email address')">
|
||||
<b-input
|
||||
type="email"
|
||||
v-model="anonymousParticipation.email"
|
||||
:placeholder="$t('Your email')"
|
||||
required
|
||||
></b-input>
|
||||
</b-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>
|
||||
<b-field :label="$t('Message')">
|
||||
<b-input
|
||||
type="textarea"
|
||||
v-model="anonymousParticipation.message"
|
||||
minlength="10"
|
||||
:required="event.joinOptions === EventJoinOptions.RESTRICTED"
|
||||
></b-input>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-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>
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
<b-button
|
||||
:disabled="sendingForm"
|
||||
type="is-primary"
|
||||
native-type="submit"
|
||||
>{{ $t("Send email") }}</b-button
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<b-button
|
||||
native-type="button"
|
||||
tag="a"
|
||||
type="is-text"
|
||||
@click="$router.go(-1)"
|
||||
>{{ $t("Back to previous page") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<h1 class="title">
|
||||
{{ $t("Request for participation confirmation sent") }}
|
||||
</h1>
|
||||
<p class="content">
|
||||
<span>{{
|
||||
$t("Check your inbox (and your junk mail folder).")
|
||||
}}</span>
|
||||
<span
|
||||
class="details"
|
||||
v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
<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(
|
||||
"Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation."
|
||||
"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."
|
||||
)
|
||||
}} </span
|
||||
><span class="details" v-else>{{
|
||||
}}
|
||||
</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."
|
||||
"Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation."
|
||||
)
|
||||
}}</span>
|
||||
</p>
|
||||
<b-message type="is-warning" v-if="error">{{ error }}</b-message>
|
||||
<p class="content">
|
||||
<i18n path="You may now close this window, or {return_to_event}.">
|
||||
}} </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
|
||||
slot="return_to_event"
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||
>{{ $t("return to the event's page") }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<b-message type="is-danger" v-else-if="!$apollo.loading"
|
||||
<o-notification variant="danger" v-else-if="!loading"
|
||||
>{{
|
||||
$t(
|
||||
"Unable to load event for participation. The error details are provided below:"
|
||||
@@ -121,162 +131,147 @@
|
||||
<details>
|
||||
<pre>{{ error }}</pre>
|
||||
</details>
|
||||
</b-message>
|
||||
</o-notification>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { EventModel, IEvent } from "@/types/event.model";
|
||||
<script lang="ts" setup>
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
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";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
event: {
|
||||
const error = ref<boolean | string>(false);
|
||||
|
||||
const { anonymousActorId } = useAnonymousActorId();
|
||||
|
||||
const props = defineProps<{
|
||||
uuid: string;
|
||||
}>();
|
||||
const { event, loading } = useFetchEventBasic(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() {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
};
|
||||
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,
|
||||
},
|
||||
},
|
||||
error(e) {
|
||||
this.error = e;
|
||||
},
|
||||
skip() {
|
||||
return !this.uuid;
|
||||
},
|
||||
update: (data) => new EventModel(data.event),
|
||||
},
|
||||
config: CONFIG,
|
||||
});
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Participation without account") as string,
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class ParticipationWithoutAccount extends Vue {
|
||||
@Prop({ type: String, required: true }) uuid!: string;
|
||||
}));
|
||||
|
||||
anonymousParticipation: {
|
||||
email: string;
|
||||
message: string;
|
||||
saveParticipation: boolean;
|
||||
} = {
|
||||
email: "",
|
||||
message: "",
|
||||
saveParticipation: true,
|
||||
};
|
||||
|
||||
event!: IEvent;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
error: string | boolean = false;
|
||||
|
||||
formSent = false;
|
||||
|
||||
sendingForm = false;
|
||||
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async joinEvent(): Promise<void> {
|
||||
this.error = false;
|
||||
this.sendingForm = true;
|
||||
joinEventDone(async ({ data }) => {
|
||||
sendingForm.value = false;
|
||||
formSent.value = true;
|
||||
if (
|
||||
data?.joinEvent.metadata.cancellationToken &&
|
||||
anonymousParticipation.saveParticipation &&
|
||||
event.value
|
||||
) {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
|
||||
mutation: JOIN_EVENT,
|
||||
variables: {
|
||||
eventId: this.event.id,
|
||||
actorId: this.config.anonymous.actorId,
|
||||
email: this.anonymousParticipation.email,
|
||||
message: this.anonymousParticipation.message,
|
||||
locale: this.$i18n.locale,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
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: this.event.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: this.event.uuid },
|
||||
data: {
|
||||
event: {
|
||||
...cachedData.event,
|
||||
participantStats,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
this.formSent = true;
|
||||
if (
|
||||
data &&
|
||||
data.joinEvent.metadata.cancellationToken &&
|
||||
this.anonymousParticipation.saveParticipation
|
||||
) {
|
||||
await addLocalUnconfirmedAnonymousParticipation(
|
||||
this.event,
|
||||
data.joinEvent.metadata.cancellationToken
|
||||
);
|
||||
}
|
||||
await addLocalUnconfirmedAnonymousParticipation(
|
||||
event.value,
|
||||
data.joinEvent.metadata.cancellationToken
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (
|
||||
["TextEncoder is not defined", "crypto.subtle is undefined"].includes(
|
||||
e.message
|
||||
)
|
||||
) {
|
||||
this.error = this.$t(
|
||||
"Unable to save your participation in this browser."
|
||||
) as string;
|
||||
} else if (e.graphQLErrors && e.graphQLErrors.length > 0) {
|
||||
this.error = e.graphQLErrors[0].message;
|
||||
} else if (e.networkError) {
|
||||
this.error = e.networkError.message;
|
||||
error.value = t("Unable to save your participation in this browser.");
|
||||
}
|
||||
}
|
||||
this.sendingForm = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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.value,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
section.container.section {
|
||||
background: $white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<section class="section container hero">
|
||||
<section class="container mx-auto hero">
|
||||
<div class="hero-body" v-if="event">
|
||||
<div class="container">
|
||||
<subtitle>{{
|
||||
$t("You wish to participate to the following event")
|
||||
}}</subtitle>
|
||||
<div class="container mx-auto">
|
||||
<h2 class="text-2xl">
|
||||
{{ $t("You wish to participate to the following event") }}
|
||||
</h2>
|
||||
<EventListViewCard v-if="event" :event="event" />
|
||||
<div class="columns has-text-centered">
|
||||
<div class="column">
|
||||
@@ -17,9 +17,9 @@
|
||||
alt="Profile illustration"
|
||||
/>
|
||||
</figure>
|
||||
<b-button type="is-primary">{{
|
||||
<o-button variant="primary">{{
|
||||
$t("I have a Mobilizon account")
|
||||
}}</b-button>
|
||||
}}</o-button>
|
||||
</router-link>
|
||||
<p>
|
||||
<small>
|
||||
@@ -32,7 +32,7 @@
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<b-tooltip
|
||||
<o-tooltip
|
||||
type="is-dark"
|
||||
:label="
|
||||
$t(
|
||||
@@ -40,8 +40,8 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<b-icon size="is-small" icon="help-circle-outline" />
|
||||
</b-tooltip>
|
||||
<o-icon size="small" icon="help-circle-outline" />
|
||||
</o-tooltip>
|
||||
</p>
|
||||
</div>
|
||||
<vertical-divider
|
||||
@@ -65,9 +65,9 @@
|
||||
alt="Privacy illustration"
|
||||
/>
|
||||
</figure>
|
||||
<b-button type="is-primary">{{
|
||||
<o-button variant="primary">{{
|
||||
$t("I don't have a Mobilizon account")
|
||||
}}</b-button>
|
||||
}}</o-button>
|
||||
</router-link>
|
||||
<a :href="`${event.url}/participate/without-account`" v-else>
|
||||
<figure class="image is-128x128">
|
||||
@@ -76,9 +76,9 @@
|
||||
alt="Privacy illustration"
|
||||
/>
|
||||
</figure>
|
||||
<b-button type="is-primary">{{
|
||||
<o-button variant="primary">{{
|
||||
$t("I don't have a Mobilizon account")
|
||||
}}</b-button>
|
||||
}}</o-button>
|
||||
</a>
|
||||
<p>
|
||||
<small>{{ $t("Participate using your email address") }}</small>
|
||||
@@ -90,78 +90,56 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-text-centered">
|
||||
<b-button tag="a" type="is-text" @click="$router.go(-1)">{{
|
||||
<o-button tag="a" type="is-text" @click="router.go(-1)">{{
|
||||
$t("Back to previous page")
|
||||
}}</b-button>
|
||||
}}</o-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { FETCH_EVENT } from "@/graphql/event";
|
||||
<script lang="ts" setup>
|
||||
import EventListViewCard from "@/components/Event/EventListViewCard.vue";
|
||||
import { EventModel, IEvent } from "@/types/event.model";
|
||||
import VerticalDivider from "@/components/Utils/VerticalDivider.vue";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import Subtitle from "@/components/Utils/Subtitle.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";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
VerticalDivider,
|
||||
EventListViewCard,
|
||||
Subtitle,
|
||||
},
|
||||
apollo: {
|
||||
event: {
|
||||
query: FETCH_EVENT,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.uuid;
|
||||
},
|
||||
update: (data) => new EventModel(data.event),
|
||||
},
|
||||
config: CONFIG,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Unlogged participation") as string,
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class UnloggedParticipation extends Vue {
|
||||
@Prop({ type: String, required: true }) uuid!: string;
|
||||
const props = defineProps<{ uuid: string }>();
|
||||
|
||||
RouteName = RouteName;
|
||||
const { event } = useFetchEvent(props.uuid);
|
||||
|
||||
event!: IEvent;
|
||||
const { anonymousParticipationConfig } = useAnonymousParticipationConfig();
|
||||
|
||||
config!: IConfig;
|
||||
const router = useRouter();
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get host(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
get anonymousParticipationAllowed(): boolean {
|
||||
return this.event.options.anonymousParticipation;
|
||||
}
|
||||
useHead({
|
||||
title: computed(() => t("Unlogged participation")),
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
});
|
||||
|
||||
get hasAnonymousEmailParticipationMethod(): boolean {
|
||||
const host = computed((): string => {
|
||||
return window.location.hostname;
|
||||
});
|
||||
|
||||
const anonymousParticipationAllowed = computed((): boolean | undefined => {
|
||||
return event.value?.options.anonymousParticipation;
|
||||
});
|
||||
|
||||
const hasAnonymousEmailParticipationMethod = computed(
|
||||
(): boolean | undefined => {
|
||||
return (
|
||||
this.config.anonymous.participation.allowed &&
|
||||
this.config.anonymous.participation.validation.email.enabled
|
||||
anonymousParticipationConfig.value?.allowed &&
|
||||
anonymousParticipationConfig.value?.validation.email.enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.column > a {
|
||||
|
||||
Reference in New Issue
Block a user