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:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>