Migrate to Vue 3 and Vite

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2022-07-12 10:55:28 +02:00
parent 8f4099ee33
commit ee20e03cc2
464 changed files with 31515 additions and 32758 deletions

View File

@@ -1,7 +1,13 @@
<template>
<article class="box mb-5 mt-4">
<div class="identity-header" dir="auto">
<figure class="image is-24x24" v-if="participation.actor.avatar">
<article class="bg-white dark:bg-mbz-purple mb-5 mt-4 p-0">
<div
class="bg-mbz-yellow-2 flex p-2 text-violet-title rounded-t-lg"
dir="auto"
>
<figure
class="image is-24x24 ltr:pr-1 rtl:pl-1"
v-if="participation.actor.avatar"
>
<img
class="is-rounded"
:src="participation.actor.avatar.url"
@@ -10,20 +16,23 @@
width="24"
/>
</figure>
<b-icon v-else icon="account-circle" />
<AccountCircle class="ltr:pr-1 rtl:pl-1" v-else />
{{ displayNameAndUsername(participation.actor) }}
</div>
<div class="list-card">
<div class="content-and-actions">
<div class="event-preview mr-0 ml-0">
<div>
<div class="date-component">
<div class="list-card flex flex-col relative">
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-1.5 gapt-x-3"
>
<div class="mr-0 ml-0">
<div class="h-36 relative w-full">
<div class="flex absolute bottom-2 left-2 z-10">
<date-calendar-icon
:date="participation.event.beginsOn"
:date="participation.event.beginsOn.toString()"
:small="true"
/>
</div>
<router-link
class="h-full"
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
@@ -43,10 +52,10 @@
</router-link>
</div>
</div>
<div class="list-card-content">
<div class="title-wrapper" dir="auto">
<div class="list-card-content lg:col-span-2 flex-1 p-2">
<div class="flex items-center pt-2" dir="auto">
<b-tag
type="is-info"
variant="info"
class="mr-1 mb-1"
size="is-medium"
v-if="participation.event.status === EventStatus.TENTATIVE"
@@ -54,7 +63,7 @@
{{ $t("Tentative") }}
</b-tag>
<b-tag
type="is-danger"
variant="danger"
class="mr-1 mb-1"
size="is-medium"
v-if="participation.event.status === EventStatus.CANCELLED"
@@ -67,52 +76,46 @@
params: { uuid: participation.event.uuid },
}"
>
<h3 class="title" :lang="participation.event.language">
<h3
class="line-clamp-3 font-bold mx-auto my-0 text-lg text-violet-title dark:text-white"
:lang="participation.event.language"
>
{{ participation.event.title }}
</h3>
</router-link>
</div>
<inline-address
v-if="participation.event.physicalAddress"
class="event-subtitle"
:physical-address="participation.event.physicalAddress"
/>
<div
class="event-subtitle"
v-else-if="
participation.event.options &&
participation.event.options.isOnline
"
>
<b-icon icon="video" />
<Video />
<span>{{ $t("Online") }}</span>
</div>
<div class="event-subtitle event-organizer">
<figure
class="image is-24x24"
v-if="
organizer(participation.event) &&
organizer(participation.event).avatar
"
>
<div class="flex gap-1">
<figure class="" v-if="actorAvatarURL">
<img
class="is-rounded"
:src="organizer(participation.event).avatar.url"
class="rounded"
:src="actorAvatarURL"
alt=""
width="24"
height="24"
/>
</figure>
<b-icon v-else icon="account-circle" />
<span class="organizer-name">
<AccountCircle v-else />
<span>
{{ organizerDisplayName(participation.event) }}
</span>
</div>
<div class="event-subtitle event-participants">
<b-icon
:class="{ 'has-text-danger': lastSeatsLeft }"
icon="account-group"
/>
<div class="flex">
<AccountGroup :class="{ 'has-text-danger': lastSeatsLeft }" />
<span
class="participant-stats"
class="flex items-center py-0 px-2"
v-if="participation.role !== ParticipantRole.NOT_APPROVED"
>
<!-- Less than 10 seats left -->
@@ -129,32 +132,32 @@
"
>
{{
$tc(
$t(
"{available}/{capacity} available places",
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
{
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
},
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant
)
}}
</span>
<span v-else>
{{
$tc(
$t(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
},
participation.event.participantStats.participant
)
}}
</span>
<b-button
<o-button
v-if="participation.event.participantStats.notApproved > 0"
type="is-text"
@click="
@@ -166,32 +169,36 @@
"
>
{{
$tc(
$t(
"{count} requests waiting",
participation.event.participantStats.notApproved,
{
count: participation.event.participantStats.notApproved,
}
},
participation.event.participantStats.notApproved
)
}}
</b-button>
</o-button>
</span>
</div>
</div>
<div class="actions">
<b-dropdown aria-role="list" position="is-bottom-left">
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
{{ $t("Actions") }}
</b-button>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
<o-dropdown aria-role="list">
<template #trigger>
<o-button icon-right="dots-horizontal">
{{ $t("Actions") }}
</o-button>
</template>
<o-dropdown-item
aria-role="listitem"
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<div
class="flex gap-1"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
@@ -199,13 +206,17 @@
})
"
>
<b-icon icon="pencil" />
<Pencil />
{{ $t("Edit") }}
</b-dropdown-item>
</div>
</o-dropdown-item>
<b-dropdown-item
v-if="participation.role === ParticipantRole.CREATOR"
aria-role="listitem"
<o-dropdown-item
aria-role="listitem"
v-if="participation.role === ParticipantRole.CREATOR"
>
<div
class="flex gap-1"
@click="
gotToWithCheck(participation, {
name: RouteName.DUPLICATE_EVENT,
@@ -213,32 +224,37 @@
})
"
>
<b-icon icon="content-duplicate" />
<ContentDuplicate />
{{ $t("Duplicate") }}
</b-dropdown-item>
</div>
</o-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="openDeleteEventModalWrapper"
>
<b-icon icon="delete" />
<o-dropdown-item
aria-role="listitem"
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<div @click="openDeleteEventModalWrapper" class="flex gap-1">
<Delete />
{{ $t("Delete") }}
</b-dropdown-item>
</div>
</o-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
<o-dropdown-item
aria-role="listitem"
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<div
class="flex gap-1"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
@@ -246,304 +262,383 @@
})
"
>
<b-icon icon="account-multiple-plus" />
<AccountMultiplePlus />
{{ $t("Manage participations") }}
</b-dropdown-item>
</div>
</o-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<b-icon icon="view-compact" />
{{ $t("View event page") }}
</router-link>
</b-dropdown-item>
</b-dropdown>
</div>
<o-dropdown-item aria-role="listitem">
<router-link
class="flex gap-1"
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<ViewCompact />
{{ $t("View event page") }}
</router-link>
</o-dropdown-item>
</o-dropdown>
</div>
</div>
</article>
</template>
<script lang="ts">
import { Component, Prop } from "vue-property-decorator";
<script lang="ts" setup>
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { mixins } from "vue-class-component";
import { RawLocation, Route } from "vue-router";
import { EventStatus, EventVisibility, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import { IParticipant } from "@/types/participant.model";
import {
IEvent,
IEventCardOptions,
organizer,
organizerAvatarUrl,
organizerDisplayName,
} from "../../types/event.model";
import { displayNameAndUsername, IActor, IPerson } from "../../types/actor";
import ActorMixin from "../../mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import EventMixin from "../../mixins/event";
import RouteName from "../../router/name";
import { changeIdentity } from "../../utils/auth";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
} from "@/types/event.model";
import { displayNameAndUsername, IActor, IPerson } from "@/types/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import RouteName from "@/router/name";
import { changeIdentity } from "@/utils/identity";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
import InlineAddress from "@/components/Address/InlineAddress.vue";
import { PropType } from "vue";
import { RouteLocationRaw, useRouter } from "vue-router";
import Pencil from "vue-material-design-icons/Pencil.vue";
import ContentDuplicate from "vue-material-design-icons/ContentDuplicate.vue";
import Delete from "vue-material-design-icons/Delete.vue";
import AccountMultiplePlus from "vue-material-design-icons/AccountMultiplePlus.vue";
import ViewCompact from "vue-material-design-icons/ViewCompact.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
import Video from "vue-material-design-icons/Video.vue";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import { computed, inject } from "vue";
import { useQuery } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n";
import { Dialog } from "@/plugins/dialog";
import { Snackbar } from "@/plugins/snackbar";
import { useDeleteEvent } from "@/composition/apollo/event";
const defaultOptions: IEventCardOptions = {
hideDate: true,
loggedPerson: false,
hideDetails: false,
organizerActor: null,
memberofGroup: false,
};
@Component({
components: {
DateCalendarIcon,
PopoverActorCard,
LazyImageWrapper,
InlineAddress,
},
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
const props = withDefaults(
defineProps<{
participation: IParticipant;
options: IEventCardOptions;
}>(),
{
options: () => ({
hideDate: true,
loggedPerson: false,
hideDetails: false,
organizerActor: null,
}),
}
);
const emit = defineEmits(["eventDeleted"]);
const { result: currentActorResult } = useQuery(CURRENT_ACTOR_CLIENT);
const currentActor = computed(() => currentActorResult.value?.currentActor);
const { t } = useI18n({ useScope: "global" });
const mergedOptions = computed<IEventCardOptions>(() => {
return { ...defaultOptions, ...props.options };
});
const dialog = inject<Dialog>("dialog");
const openDeleteEventModal = (
event: IEvent,
callback: (anEvent: IEvent) => any
): void => {
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
const participantsLength = event.participantStats.participant;
const prefix = participantsLength
? t(
"There are {participants} participants.",
{
participants: participantsLength,
},
participantsLength
)
: "";
dialog?.prompt({
variant: "danger",
title: t("Delete event"),
message: `${prefix}
${t(
"Are you sure you want to delete this event? This action cannot be reverted."
)}
<br><br>
${t('To confirm, type your event title "{eventTitle}"', {
eventTitle: event.title,
})}`,
confirmText: t("Delete {eventTitle}", {
eventTitle: event.title,
}),
inputAttrs: {
placeholder: event.title,
pattern: escapeRegExp(event.title),
},
},
})
export default class EventParticipationCard extends mixins(
ActorMixin,
EventMixin
) {
onConfirm: () => callback(event),
});
};
const { oruga } = useProgrammatic();
const snackbar = inject<Snackbar>("snackbar");
const {
mutate: deleteEvent,
onDone: onDeleteEventDone,
onError: onDeleteEventError,
} = useDeleteEvent();
onDeleteEventDone(() => {
/**
* The participation associated
* When the event corresponding has been deleted (by the organizer).
* A notification is already triggered.
*
* @type {string}
*/
@Prop({ required: true, type: Object as PropType<IParticipant> })
participation!: IParticipant;
emit("eventDeleted", props.participation.event.id);
/**
* Options are merged with default options
*/
@Prop({ required: false, default: () => defaultOptions })
options!: IEventCardOptions;
oruga.notification.open({
message: t("Event {eventTitle} deleted", {
eventTitle: props.participation.event.title,
}),
variant: "success",
position: "bottom-right",
duration: 5000,
});
});
currentActor!: IPerson;
onDeleteEventError((error) => {
snackbar?.open({
message: error.message,
variant: "danger",
position: "bottom",
});
ParticipantRole = ParticipantRole;
console.error(error);
});
EventVisibility = EventVisibility;
/**
* Delete the event
*/
const openDeleteEventModalWrapper = () => {
openDeleteEventModal(
props.participation.event,
deleteEvent(props.participation.event)
);
};
displayNameAndUsername = displayNameAndUsername;
const router = useRouter();
organizerDisplayName = organizerDisplayName;
organizer = organizer;
RouteName = RouteName;
EventStatus = EventStatus;
get mergedOptions(): IEventCardOptions {
return { ...defaultOptions, ...this.options };
const gotToWithCheck = async (
participation: IParticipant,
route: RouteLocationRaw
): Promise<any> => {
if (
participation.actor.id !== currentActor.value.id &&
participation.event.organizerActor
) {
const organizerActor = participation.event.organizerActor as IPerson;
await changeIdentity(organizerActor);
oruga.notification.open({
message: t(
"Current identity has been changed to {identityName} in order to manage this event.",
{
identityName: organizerActor.preferredUsername,
}
),
variant: "info",
position: "bottom-right",
duration: 5000,
});
}
return router.push(route);
};
/**
* Delete the event
*/
async openDeleteEventModalWrapper(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.openDeleteEventModal(this.participation.event);
const organizerActor = computed<IActor | undefined>(() => {
if (
props.participation.event.attributedTo &&
props.participation.event.attributedTo.id
) {
return props.participation.event.attributedTo;
}
return props.participation.event.organizerActor;
});
async gotToWithCheck(
participation: IParticipant,
route: RawLocation
): Promise<Route> {
if (
participation.actor.id !== this.currentActor.id &&
participation.event.organizerActor
) {
const organizerActor = participation.event.organizerActor as IPerson;
await changeIdentity(this.$apollo.provider.defaultClient, organizerActor);
this.$buefy.notification.open({
message: this.$t(
"Current identity has been changed to {identityName} in order to manage this event.",
{
identityName: organizerActor.preferredUsername,
}
) as string,
type: "is-info",
position: "is-bottom-right",
duration: 5000,
});
}
return this.$router.push(route);
const seatsLeft = computed<number | null>(() => {
if (props.participation.event.options.maximumAttendeeCapacity > 0) {
return (
props.participation.event.options.maximumAttendeeCapacity -
props.participation.event.participantStats.participant
);
}
return null;
});
get organizerActor(): IActor | undefined {
if (
this.participation.event.attributedTo &&
this.participation.event.attributedTo.id
) {
return this.participation.event.attributedTo;
}
return this.participation.event.organizerActor;
const lastSeatsLeft = computed<boolean>(() => {
if (seatsLeft.value) {
return seatsLeft.value < 10;
}
return false;
});
get seatsLeft(): number | null {
if (this.participation.event.options.maximumAttendeeCapacity > 0) {
return (
this.participation.event.options.maximumAttendeeCapacity -
this.participation.event.participantStats.participant
);
}
return null;
}
const actorAvatarURL = computed<string | null>(() =>
organizerAvatarUrl(props.participation.event)
);
get lastSeatsLeft(): boolean {
if (this.seatsLeft) {
return this.seatsLeft < 10;
}
return false;
}
}
// export default class EventParticipationCard extends mixins(
// ActorMixin,
// EventMixin
// ) {
// }
</script>
<style lang="scss" scoped>
@use "@/styles/_mixins" as *;
@use "@/styles/_event-card";
@import "~bulma/sass/utilities/mixins.sass";
// @import "node_modules/bulma/sass/utilities/mixins.sass";
article.box {
div.tag-container {
position: absolute;
top: 10px;
right: 0;
@include margin-left(-5px);
z-index: 10;
max-width: 40%;
// div.tag-container {
// position: absolute;
// top: 10px;
// right: 0;
// @include margin-left(-5px);
// z-index: 10;
// max-width: 40%;
span.tag {
margin: 5px auto;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 1);
/*word-break: break-all;*/
text-overflow: ellipsis;
overflow: hidden;
display: block;
/*text-align: right;*/
font-size: 1em;
/*padding: 0 1px;*/
line-height: 1.75em;
}
}
// span.tag {
// margin: 5px auto;
// box-shadow: 0 0 5px 0 rgba(0, 0, 0, 1);
// /*word-break: break-all;*/
// text-overflow: ellipsis;
// overflow: hidden;
// display: block;
// /*text-align: right;*/
// font-size: 1em;
// /*padding: 0 1px;*/
// line-height: 1.75em;
// }
// }
.list-card {
display: flex;
padding: 0 6px 0 0;
position: relative;
flex-direction: column;
// display: flex;
// padding: 0 6px 0 0;
// position: relative;
// flex-direction: column;
.content-and-actions {
display: grid;
grid-gap: 5px 10px;
// display: grid;
// grid-gap: 5px 10px;
grid-template-areas: "preview" "body" "actions";
@include tablet {
grid-template-columns: 1fr 3fr;
grid-template-areas: "preview body" "actions actions";
}
// @include tablet {
// grid-template-columns: 1fr 3fr;
// grid-template-areas: "preview body" "actions actions";
// }
@include desktop {
grid-template-columns: 1fr 3fr 1fr;
grid-template-areas: "preview body actions";
}
// @include desktop {
// grid-template-columns: 1fr 3fr 1fr;
// grid-template-areas: "preview body actions";
// }
.event-preview {
grid-area: preview;
& > div {
height: 128px;
width: 100%;
position: relative;
// width: 100%;
// position: relative;
div.date-component {
display: flex;
position: absolute;
bottom: 5px;
left: 5px;
z-index: 1;
}
// div.date-component {
// display: flex;
// position: absolute;
// bottom: 5px;
// left: 5px;
// z-index: 1;
// }
img {
width: 100%;
object-position: center;
object-fit: cover;
height: 100%;
}
// img {
// width: 100%;
// object-position: center;
// object-fit: cover;
// height: 100%;
// }
}
}
.actions {
padding: 7px;
cursor: pointer;
align-self: center;
justify-self: center;
// padding: 7px;
// cursor: pointer;
// align-self: center;
// justify-self: center;
grid-area: actions;
}
div.list-card-content {
flex: 1;
padding: 5px;
// flex: 1;
// padding: 5px;
grid-area: body;
.participant-stats {
display: flex;
align-items: center;
padding: 0 5px;
}
// .participant-stats {
// display: flex;
// align-items: center;
// padding: 0 5px;
// }
div.title-wrapper {
display: flex;
align-items: center;
padding-top: 5px;
// div.title-wrapper {
// display: flex;
// align-items: center;
// padding-top: 5px;
a {
text-decoration: none;
padding-bottom: 5px;
}
// a {
// text-decoration: none;
// padding-bottom: 5px;
// }
.title {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 18px;
line-height: 24px;
margin: auto 0;
font-weight: bold;
color: $title-color;
}
}
// .title {
// display: -webkit-box;
// -webkit-line-clamp: 3;
// -webkit-box-orient: vertical;
// overflow: hidden;
// font-size: 18px;
// line-height: 24px;
// margin: auto 0;
// font-weight: bold;
// color: $title-color;
// }
// }
}
}
}
.identity-header {
background: $yellow-2;
display: flex;
padding: 5px;
// .identity-header {
// background: $yellow-2;
// display: flex;
// padding: 5px;
figure,
span.icon {
@include padding-right(3px);
}
}
// figure,
// span.icon {
// @include padding-right(3px);
// }
// }
& > .columns {
padding: 1.25rem;
}
padding: 0;
// & > .columns {
// padding: 1.25rem;
// }
// padding: 0;
}
</style>