Migrate to Vue 3 and Vite
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<h1>{{ $t("Event list") }}</h1>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<div v-if="events.length > 0" class="columns is-multiline">
|
||||
<EventCard
|
||||
v-for="event in events"
|
||||
:key="event.uuid"
|
||||
:event="event"
|
||||
class="column is-one-quarter-desktop is-half-mobile"
|
||||
/>
|
||||
</div>
|
||||
<b-message
|
||||
v-if-else="events.length === 0 && $apollo.loading === false"
|
||||
type="is-danger"
|
||||
>{{ $t("No events found") }}</b-message
|
||||
>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import EventCard from "../../components/Event/EventCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EventCard,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Event list") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class EventList extends Vue {
|
||||
@Prop(String) location!: string;
|
||||
|
||||
events = [];
|
||||
|
||||
loading = true;
|
||||
|
||||
locationChip = false;
|
||||
|
||||
locationText = "";
|
||||
|
||||
viewEvent(event: IEvent): void {
|
||||
this.$router.push({ name: RouteName.EVENT, params: { uuid: event.uuid } });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped></style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container section" v-if="group">
|
||||
<div class="container mx-auto" v-if="group">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{
|
||||
@@ -8,14 +8,14 @@
|
||||
text: displayName(group),
|
||||
},
|
||||
{
|
||||
name: RouteName.EVENTS,
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
text: $t('Events'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<section>
|
||||
<h1 class="title" v-if="group">
|
||||
<h1 class="" v-if="group">
|
||||
{{
|
||||
$t("{group}'s events", {
|
||||
group: displayName(group),
|
||||
@@ -29,23 +29,24 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<router-link
|
||||
<o-button
|
||||
tag="router-link"
|
||||
variant="primary"
|
||||
v-if="isCurrentActorAGroupModerator"
|
||||
:to="{
|
||||
name: RouteName.CREATE_EVENT,
|
||||
query: { actorId: group.id },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ $t("+ Create an event") }}</router-link
|
||||
>{{ $t("+ Create an event") }}</o-button
|
||||
>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<o-loading v-model:active="groupLoading"></o-loading>
|
||||
<section v-if="group">
|
||||
<subtitle>
|
||||
<h2 class="text-2xl">
|
||||
{{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }}
|
||||
</subtitle>
|
||||
<b-switch class="mb-4" v-model="showPassedEvents">{{
|
||||
</h2>
|
||||
<o-switch class="mb-4" v-model="showPassedEvents">{{
|
||||
$t("Past events")
|
||||
}}</b-switch>
|
||||
}}</o-switch>
|
||||
<grouped-multi-event-minimalist-card
|
||||
:events="group.organizedEvents.elements"
|
||||
:isCurrentActorMember="isCurrentActorMember"
|
||||
@@ -53,7 +54,7 @@
|
||||
<empty-content
|
||||
v-if="
|
||||
group.organizedEvents.elements.length === 0 &&
|
||||
$apollo.loading === false
|
||||
groupLoading === false
|
||||
"
|
||||
icon="calendar"
|
||||
:inline="true"
|
||||
@@ -69,13 +70,13 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<b-button type="is-text" tag="a" :href="group.url">
|
||||
<o-button type="is-text" tag="a" :href="group.url">
|
||||
{{ $t("View the group profile on the original instance") }}
|
||||
</b-button>
|
||||
</o-button>
|
||||
</div>
|
||||
</template>
|
||||
</empty-content>
|
||||
<b-pagination
|
||||
<o-pagination
|
||||
class="mt-4"
|
||||
:total="group.organizedEvents.total"
|
||||
v-model="page"
|
||||
@@ -85,116 +86,93 @@
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
>
|
||||
</b-pagination>
|
||||
</o-pagination>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import RouteName from "@/router/name";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import GroupedMultiEventMinimalistCard from "@/components/Event/GroupedMultiEventMinimalistCard.vue";
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { FETCH_GROUP_EVENTS } from "@/graphql/event";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { displayName, IGroup, usernameWithDomain } from "../../types/actor";
|
||||
import { displayName, IPerson, usernameWithDomain } from "../../types/actor";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
booleanTransformer,
|
||||
integerTransformer,
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const EVENTS_PAGE_LIMIT = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
memberships: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: (data) => data.person.memberships.elements,
|
||||
skip() {
|
||||
return !this.currentActor || !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
group: {
|
||||
query: FETCH_GROUP_EVENTS,
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
beforeDateTime: this.showPassedEvents ? new Date() : null,
|
||||
afterDateTime: this.showPassedEvents ? null : new Date(),
|
||||
organisedEventsPage: this.page,
|
||||
organisedEventsLimit: EVENTS_PAGE_LIMIT,
|
||||
};
|
||||
},
|
||||
update: (data) => data.group,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
EmptyContent,
|
||||
Subtitle,
|
||||
GroupedMultiEventMinimalistCard,
|
||||
},
|
||||
metaInfo() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { group } = this;
|
||||
return {
|
||||
title: this.$t("{group} events", {
|
||||
group: displayName(group),
|
||||
}) as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class GroupEvents extends Vue {
|
||||
group!: IGroup;
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
memberships!: IMember[];
|
||||
const { result: membershipsResult } = useQuery<{
|
||||
person: Pick<IPerson, "memberships">;
|
||||
}>(
|
||||
PERSON_MEMBERSHIPS,
|
||||
() => ({ id: currentActor.value?.id }),
|
||||
() => ({ enabled: currentActor.value?.id !== undefined })
|
||||
);
|
||||
const memberships = computed(
|
||||
() => membershipsResult.value?.person.memberships.elements
|
||||
);
|
||||
|
||||
get page(): number {
|
||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
||||
}
|
||||
const route = useRoute();
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
const showPassedEvents = useRouteQuery(
|
||||
"showPassedEvents",
|
||||
false,
|
||||
booleanTransformer
|
||||
);
|
||||
|
||||
set page(page: number) {
|
||||
this.$router.push({
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
query: { ...this.$route.query, page: page.toString() },
|
||||
});
|
||||
this.$apollo.queries.group.refetch();
|
||||
}
|
||||
const { result: groupResult, loading: groupLoading } = useQuery(
|
||||
FETCH_GROUP_EVENTS,
|
||||
() => ({
|
||||
name: route.params.preferredUsername,
|
||||
beforeDateTime: showPassedEvents.value ? new Date() : null,
|
||||
afterDateTime: showPassedEvents.value ? null : new Date(),
|
||||
organisedEventsPage: page.value,
|
||||
organisedEventsLimit: EVENTS_PAGE_LIMIT,
|
||||
})
|
||||
);
|
||||
const group = computed(() => groupResult.value?.group);
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: t("{group} events", {
|
||||
group: displayName(group.value),
|
||||
}),
|
||||
});
|
||||
|
||||
displayName = displayName;
|
||||
const isCurrentActorMember = computed((): boolean => {
|
||||
if (!group.value || !memberships.value) return false;
|
||||
return (memberships.value ?? [])
|
||||
.map(({ parent: { id } }) => id)
|
||||
.includes(group.value.id);
|
||||
});
|
||||
|
||||
RouteName = RouteName;
|
||||
const isCurrentActorAGroupModerator = computed((): boolean => {
|
||||
return hasCurrentActorThisRole([
|
||||
MemberRole.MODERATOR,
|
||||
MemberRole.ADMINISTRATOR,
|
||||
]);
|
||||
});
|
||||
|
||||
EVENTS_PAGE_LIMIT = EVENTS_PAGE_LIMIT;
|
||||
|
||||
get isCurrentActorMember(): boolean {
|
||||
if (!this.group || !this.memberships) return false;
|
||||
return this.memberships
|
||||
.map(({ parent: { id } }) => id)
|
||||
.includes(this.group.id);
|
||||
}
|
||||
|
||||
get showPassedEvents(): boolean {
|
||||
return this.$route.query.future === "false";
|
||||
}
|
||||
|
||||
set showPassedEvents(value: boolean) {
|
||||
this.$router.replace({ query: { future: (!value).toString() } });
|
||||
}
|
||||
}
|
||||
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
|
||||
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
|
||||
return (
|
||||
memberships.value !== undefined &&
|
||||
memberships.value?.length > 0 &&
|
||||
roles.includes(memberships.value[0].role)
|
||||
);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.container.section {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
div.event-list {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,78 +1,75 @@
|
||||
<template>
|
||||
<div class="section container">
|
||||
<h1 class="title">
|
||||
{{ $t("My events") }}
|
||||
<div class="container mx-auto">
|
||||
<h1 class="text-4xl">
|
||||
{{ t("My events") }}
|
||||
</h1>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"You will find here all the events you have created or of which you are a participant, as well as events organized by groups you follow or are a member of."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="buttons" v-if="!hideCreateEventButton">
|
||||
<router-link
|
||||
class="button is-primary"
|
||||
<div class="my-2" v-if="!hideCreateEventButton">
|
||||
<o-button
|
||||
tag="router-link"
|
||||
variant="primary"
|
||||
:to="{ name: RouteName.CREATE_EVENT }"
|
||||
>{{ $t("Create event") }}</router-link
|
||||
>{{ t("Create event") }}</o-button
|
||||
>
|
||||
</div>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<div class="wrapper">
|
||||
<div class="event-filter">
|
||||
<b-field grouped group-multiline>
|
||||
<b-field>
|
||||
<b-switch v-model="showUpcoming">{{
|
||||
showUpcoming ? $t("Upcoming events") : $t("Past events")
|
||||
}}</b-switch>
|
||||
</b-field>
|
||||
<b-field v-if="showUpcoming">
|
||||
<b-checkbox v-model="showDrafts">{{ $t("Drafts") }}</b-checkbox>
|
||||
</b-field>
|
||||
<b-field v-if="showUpcoming">
|
||||
<b-checkbox v-model="showAttending">{{
|
||||
$t("Attending")
|
||||
}}</b-checkbox>
|
||||
</b-field>
|
||||
<b-field v-if="showUpcoming">
|
||||
<b-checkbox v-model="showMyGroups">{{
|
||||
$t("From my groups")
|
||||
}}</b-checkbox>
|
||||
</b-field>
|
||||
<p v-if="!showUpcoming">
|
||||
{{
|
||||
$tc(
|
||||
"You have attended {count} events in the past.",
|
||||
pastParticipations.total,
|
||||
{
|
||||
count: pastParticipations.total,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<b-field
|
||||
class="date-filter"
|
||||
expanded
|
||||
:label="
|
||||
showUpcoming
|
||||
? $t('Showing events starting on')
|
||||
: $t('Showing events before')
|
||||
"
|
||||
>
|
||||
<b-datepicker
|
||||
v-model="dateFilter"
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
/>
|
||||
<b-button
|
||||
@click="dateFilter = new Date()"
|
||||
class="reset-area"
|
||||
icon-left="close"
|
||||
:title="$t('Clear date filter field')"
|
||||
/>
|
||||
</b-field>
|
||||
</b-field>
|
||||
<!-- <o-loading v-model:active="$apollo.loading"></o-loading> -->
|
||||
<div class="wrapper flex flex-wrap gap-4 items-start">
|
||||
<div class="event-filter text-violet-1 flex-auto md:flex-none">
|
||||
<o-field>
|
||||
<o-switch v-model="showUpcoming">{{
|
||||
showUpcoming ? t("Upcoming events") : t("Past events")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
<o-field v-if="showUpcoming">
|
||||
<o-checkbox v-model="showDrafts">{{ t("Drafts") }}</o-checkbox>
|
||||
</o-field>
|
||||
<o-field v-if="showUpcoming">
|
||||
<o-checkbox v-model="showAttending">{{ t("Attending") }}</o-checkbox>
|
||||
</o-field>
|
||||
<o-field v-if="showUpcoming">
|
||||
<o-checkbox v-model="showMyGroups">{{
|
||||
t("From my groups")
|
||||
}}</o-checkbox>
|
||||
</o-field>
|
||||
<p v-if="!showUpcoming">
|
||||
{{
|
||||
t(
|
||||
"You have attended {count} events in the past.",
|
||||
{
|
||||
count: pastParticipations.total,
|
||||
},
|
||||
pastParticipations.total
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<o-field
|
||||
class="date-filter"
|
||||
expanded
|
||||
:label="
|
||||
showUpcoming
|
||||
? t('Showing events starting on')
|
||||
: t('Showing events before')
|
||||
"
|
||||
>
|
||||
<o-datepicker
|
||||
v-model="dateFilter"
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
/>
|
||||
<o-button
|
||||
@click="dateFilter = new Date()"
|
||||
class="reset-area"
|
||||
icon-left="close"
|
||||
:title="t('Clear date filter field')"
|
||||
/>
|
||||
</o-field>
|
||||
</div>
|
||||
<div class="my-events">
|
||||
<div class="my-events flex-1">
|
||||
<section
|
||||
class="py-4"
|
||||
v-if="showUpcoming && showDrafts && drafts.length > 0"
|
||||
@@ -82,13 +79,15 @@
|
||||
<section
|
||||
class="py-4"
|
||||
v-if="
|
||||
showUpcoming && monthlyFutureEvents && monthlyFutureEvents.size > 0
|
||||
showUpcoming &&
|
||||
monthlyFutureEvents &&
|
||||
monthlyFutureEvents.length > 0
|
||||
"
|
||||
>
|
||||
<transition-group name="list" tag="p">
|
||||
<div
|
||||
class="mb-5"
|
||||
v-for="month in monthlyFutureEvents"
|
||||
v-for="month of monthlyFutureEvents()"
|
||||
:key="month[0]"
|
||||
>
|
||||
<span class="upcoming-month">{{ month[0] }}</span>
|
||||
@@ -102,7 +101,8 @@
|
||||
/>
|
||||
<event-minimalist-card
|
||||
v-else-if="
|
||||
!monthParticipationsIds(month[1]).includes(element.id)
|
||||
element.id &&
|
||||
!monthParticipationsIds(month[1]).includes(element?.id)
|
||||
"
|
||||
:event="element"
|
||||
class="participation"
|
||||
@@ -111,7 +111,7 @@
|
||||
</div>
|
||||
</transition-group>
|
||||
<div class="columns is-centered">
|
||||
<b-button
|
||||
<o-button
|
||||
class="column is-narrow"
|
||||
v-if="
|
||||
hasMoreFutureParticipations &&
|
||||
@@ -119,9 +119,9 @@
|
||||
futureParticipations.length === limit
|
||||
"
|
||||
@click="loadMoreFutureParticipations"
|
||||
size="is-large"
|
||||
type="is-primary"
|
||||
>{{ $t("Load more") }}</b-button
|
||||
size="large"
|
||||
variant="primary"
|
||||
>{{ t("Load more") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
@@ -130,34 +130,34 @@
|
||||
v-if="
|
||||
showUpcoming &&
|
||||
monthlyFutureEvents &&
|
||||
monthlyFutureEvents.size === 0 &&
|
||||
!$apollo.loading
|
||||
monthlyFutureEvents.length === 0 &&
|
||||
true // !$apollo.loading
|
||||
"
|
||||
>
|
||||
<div class="img-container" :class="{ webp: supportsWebPFormat }" />
|
||||
<div class="img-container h-64" />
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"You don't have any upcoming events. Maybe try another filter?"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<i18n
|
||||
path="Do you wish to {create_event} or {explore_events}?"
|
||||
<i18n-t
|
||||
keypath="Do you wish to {create_event} or {explore_events}?"
|
||||
tag="p"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.CREATE_EVENT }"
|
||||
slot="create_event"
|
||||
>{{ $t("create an event") }}</router-link
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.SEARCH }"
|
||||
slot="explore_events"
|
||||
>{{ $t("explore the events") }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
<template v-slot:create_event>
|
||||
<router-link :to="{ name: RouteName.CREATE_EVENT }">{{
|
||||
t("create an event")
|
||||
}}</router-link>
|
||||
</template>
|
||||
<template v-slot:explore_events>
|
||||
<router-link :to="{ name: RouteName.SEARCH }">{{
|
||||
t("explore the events")
|
||||
}}</router-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="!showUpcoming && pastParticipations.elements.length > 0">
|
||||
@@ -167,7 +167,7 @@
|
||||
<event-participation-card
|
||||
v-for="participation in month[1]"
|
||||
:key="participation.id"
|
||||
:participation="participation"
|
||||
:participation="(participation as IParticipant)"
|
||||
:options="{ hideDate: false }"
|
||||
@event-deleted="eventDeleted"
|
||||
class="participation"
|
||||
@@ -175,16 +175,16 @@
|
||||
</div>
|
||||
</transition-group>
|
||||
<div class="columns is-centered">
|
||||
<b-button
|
||||
<o-button
|
||||
class="column is-narrow"
|
||||
v-if="
|
||||
hasMorePastParticipations &&
|
||||
pastParticipations.elements.length === limit
|
||||
"
|
||||
@click="loadMorePastParticipations"
|
||||
size="is-large"
|
||||
type="is-primary"
|
||||
>{{ $t("Load more") }}</b-button
|
||||
size="large"
|
||||
variant="primary"
|
||||
>{{ t("Load more") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
@@ -193,302 +193,230 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { ParticipantRole } from "@/types/enums";
|
||||
import RouteName from "@/router/name";
|
||||
import { supportsWebPFormat } from "@/utils/support";
|
||||
import { IParticipant, Participant } from "../../types/participant.model";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import { LOGGED_USER_DRAFTS } from "../../graphql/actor";
|
||||
import { EventModel, IEvent } from "../../types/event.model";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
import EventParticipationCard from "../../components/Event/EventParticipationCard.vue";
|
||||
import MultiEventMinimalistCard from "../../components/Event/MultiEventMinimalistCard.vue";
|
||||
import EventMinimalistCard from "../../components/Event/EventMinimalistCard.vue";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
import {
|
||||
LOGGED_USER_PARTICIPATIONS,
|
||||
LOGGED_USER_UPCOMING_EVENTS,
|
||||
} from "@/graphql/participant";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { booleanTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { Locale } from "date-fns";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRestrictions } from "@/composition/apollo/config";
|
||||
|
||||
type Eventable = IParticipant | IEvent;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Subtitle,
|
||||
MultiEventMinimalistCard,
|
||||
EventParticipationCard,
|
||||
EventMinimalistCard,
|
||||
},
|
||||
apollo: {
|
||||
config: CONFIG,
|
||||
userUpcomingEvents: {
|
||||
query: LOGGED_USER_UPCOMING_EVENTS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
afterDateTime: this.dateFilter,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
this.futureParticipations = data.loggedUser.participations.elements.map(
|
||||
(participation: IParticipant) => new Participant(participation)
|
||||
);
|
||||
this.groupEvents = data.loggedUser.followedGroupEvents.elements.map(
|
||||
({ event }: { event: IEvent }) => event
|
||||
);
|
||||
},
|
||||
},
|
||||
drafts: {
|
||||
query: LOGGED_USER_DRAFTS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
},
|
||||
update: (data) =>
|
||||
data.loggedUser.drafts.map((event: IEvent) => new EventModel(event)),
|
||||
},
|
||||
pastParticipations: {
|
||||
query: LOGGED_USER_PARTICIPATIONS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
beforeDateTime: this.dateFilter,
|
||||
};
|
||||
},
|
||||
update: (data) => data.loggedUser.participations,
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("My events") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class MyEvents extends Vue {
|
||||
futurePage = 1;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
pastPage = 1;
|
||||
const futurePage = ref(1);
|
||||
const pastPage = ref(1);
|
||||
const limit = ref(10);
|
||||
|
||||
limit = 10;
|
||||
|
||||
get showUpcoming(): boolean {
|
||||
return ((this.$route.query.showUpcoming as string) || "true") === "true";
|
||||
}
|
||||
|
||||
set showUpcoming(showUpcoming: boolean) {
|
||||
this.$router.push({
|
||||
name: RouteName.MY_EVENTS,
|
||||
query: { ...this.$route.query, showUpcoming: showUpcoming.toString() },
|
||||
});
|
||||
}
|
||||
|
||||
get showDrafts(): boolean {
|
||||
return ((this.$route.query.showDrafts as string) || "true") === "true";
|
||||
}
|
||||
|
||||
set showDrafts(showDrafts: boolean) {
|
||||
this.$router.push({
|
||||
name: RouteName.MY_EVENTS,
|
||||
query: { ...this.$route.query, showDrafts: showDrafts.toString() },
|
||||
});
|
||||
}
|
||||
|
||||
get showAttending(): boolean {
|
||||
return ((this.$route.query.showAttending as string) || "true") === "true";
|
||||
}
|
||||
|
||||
set showAttending(showAttending: boolean) {
|
||||
this.$router.push({
|
||||
name: RouteName.MY_EVENTS,
|
||||
query: { ...this.$route.query, showAttending: showAttending.toString() },
|
||||
});
|
||||
}
|
||||
|
||||
get showMyGroups(): boolean {
|
||||
return ((this.$route.query.showMyGroups as string) || "false") === "true";
|
||||
}
|
||||
|
||||
set showMyGroups(showMyGroups: boolean) {
|
||||
this.$router.push({
|
||||
name: RouteName.MY_EVENTS,
|
||||
query: { ...this.$route.query, showMyGroups: showMyGroups.toString() },
|
||||
});
|
||||
}
|
||||
|
||||
get dateFilter(): Date {
|
||||
const query = this.$route.query.dateFilter as string;
|
||||
const showUpcoming = useRouteQuery("showUpcoming", true, booleanTransformer);
|
||||
const showDrafts = useRouteQuery("showDrafts", true, booleanTransformer);
|
||||
const showAttending = useRouteQuery("showAttending", true, booleanTransformer);
|
||||
const showMyGroups = useRouteQuery("showMyGroups", false, booleanTransformer);
|
||||
const dateFilter = useRouteQuery("dateFilter", new Date(), {
|
||||
fromQuery(query) {
|
||||
if (query && /(\d{4}-\d{2}-\d{2})/.test(query)) {
|
||||
return new Date(`${query}T00:00:00Z`);
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
|
||||
set dateFilter(date: Date) {
|
||||
},
|
||||
toQuery(value: Date) {
|
||||
const pad = (number: number) => {
|
||||
if (number < 10) {
|
||||
return "0" + number;
|
||||
}
|
||||
return number;
|
||||
};
|
||||
const stringifiedDate = `${date.getFullYear()}-${pad(
|
||||
date.getMonth() + 1
|
||||
)}-${pad(date.getDate())}`;
|
||||
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(
|
||||
value.getDate()
|
||||
)}`;
|
||||
},
|
||||
});
|
||||
|
||||
if (this.$route.query.dateFilter !== stringifiedDate) {
|
||||
this.$router.push({
|
||||
name: RouteName.MY_EVENTS,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
dateFilter: stringifiedDate,
|
||||
},
|
||||
});
|
||||
const hasMoreFutureParticipations = ref(true);
|
||||
const hasMorePastParticipations = ref(true);
|
||||
|
||||
// config: CONFIG
|
||||
|
||||
const {
|
||||
result: loggedUserUpcomingEventsResult,
|
||||
fetchMore: fetchMoreUpcomingEvents,
|
||||
} = useQuery<{
|
||||
loggedUser: IUser;
|
||||
}>(LOGGED_USER_UPCOMING_EVENTS, () => ({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
afterDateTime: dateFilter.value,
|
||||
}));
|
||||
|
||||
const futureParticipations = computed(
|
||||
() =>
|
||||
loggedUserUpcomingEventsResult.value?.loggedUser.participations.elements ??
|
||||
[]
|
||||
);
|
||||
const groupEvents = computed(
|
||||
() =>
|
||||
loggedUserUpcomingEventsResult.value?.loggedUser.followedGroupEvents
|
||||
.elements ?? []
|
||||
);
|
||||
|
||||
const { result: draftsResult } = useQuery<{
|
||||
loggedUser: Pick<IUser, "drafts">;
|
||||
}>(LOGGED_USER_DRAFTS, () => ({ page: 1, limit: 10 }));
|
||||
const drafts = computed(() => draftsResult.value?.loggedUser.drafts ?? []);
|
||||
|
||||
const { result: participationsResult, fetchMore: fetchMoreParticipations } =
|
||||
useQuery<{
|
||||
loggedUser: Pick<IUser, "participations">;
|
||||
}>(LOGGED_USER_PARTICIPATIONS, () => ({ page: 1, limit: 10 }));
|
||||
const pastParticipations = computed(
|
||||
() =>
|
||||
participationsResult.value?.loggedUser.participations ?? {
|
||||
elements: [],
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
config!: IConfig;
|
||||
// metaInfo() {
|
||||
// return {
|
||||
// title: this.t("My events") as string,
|
||||
// };
|
||||
// },
|
||||
|
||||
futureParticipations: IParticipant[] = [];
|
||||
|
||||
groupEvents: IEvent[] = [];
|
||||
|
||||
hasMoreFutureParticipations = true;
|
||||
|
||||
pastParticipations: Paginate<IParticipant> = { elements: [], total: 0 };
|
||||
|
||||
hasMorePastParticipations = true;
|
||||
|
||||
drafts: IEvent[] = [];
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
supportsWebPFormat = supportsWebPFormat;
|
||||
|
||||
static monthlyEvents(
|
||||
elements: Eventable[],
|
||||
revertSort = false
|
||||
): Map<string, Eventable[]> {
|
||||
const res = elements.filter((element: Eventable) => {
|
||||
if ("role" in element) {
|
||||
return (
|
||||
element.event.beginsOn != null &&
|
||||
element.role !== ParticipantRole.REJECTED
|
||||
);
|
||||
}
|
||||
return element.beginsOn != null;
|
||||
const monthlyEvents = (
|
||||
elements: Eventable[],
|
||||
revertSort = false
|
||||
): Map<string, Eventable[]> => {
|
||||
const res = elements.filter((element: Eventable) => {
|
||||
if ("role" in element) {
|
||||
return (
|
||||
element.event.beginsOn != null &&
|
||||
element.role !== ParticipantRole.REJECTED
|
||||
);
|
||||
}
|
||||
return element.beginsOn != null;
|
||||
});
|
||||
if (revertSort) {
|
||||
res.sort((a: Eventable, b: Eventable) => {
|
||||
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
|
||||
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
|
||||
return new Date(bTime).getTime() - new Date(aTime).getTime();
|
||||
});
|
||||
if (revertSort) {
|
||||
res.sort((a: Eventable, b: Eventable) => {
|
||||
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
|
||||
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
|
||||
return new Date(bTime).getTime() - new Date(aTime).getTime();
|
||||
});
|
||||
} else {
|
||||
res.sort((a: Eventable, b: Eventable) => {
|
||||
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
|
||||
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
|
||||
return new Date(aTime).getTime() - new Date(bTime).getTime();
|
||||
});
|
||||
}
|
||||
return res.reduce((acc: Map<string, Eventable[]>, element: Eventable) => {
|
||||
const month = new Date(
|
||||
"role" in element ? element.event.beginsOn : element.beginsOn
|
||||
).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
});
|
||||
const filteredElements: Eventable[] = acc.get(month) || [];
|
||||
filteredElements.push(element);
|
||||
acc.set(month, filteredElements);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
get monthlyFutureEvents(): Map<string, Eventable[]> {
|
||||
let eventable = [] as Eventable[];
|
||||
if (this.showAttending) {
|
||||
eventable = [...eventable, ...this.futureParticipations];
|
||||
}
|
||||
if (this.showMyGroups) {
|
||||
eventable = [...eventable, ...this.groupEvents];
|
||||
}
|
||||
return MyEvents.monthlyEvents(eventable);
|
||||
}
|
||||
|
||||
get monthlyPastParticipations(): Map<string, Eventable[]> {
|
||||
return MyEvents.monthlyEvents(this.pastParticipations.elements, true);
|
||||
}
|
||||
|
||||
monthParticipationsIds(elements: Eventable[]): string[] {
|
||||
const res = elements.filter((element: Eventable) => {
|
||||
return "role" in element;
|
||||
}) as IParticipant[];
|
||||
return res.map(({ event }: { event: IEvent }) => {
|
||||
return event.id as string;
|
||||
} else {
|
||||
res.sort((a: Eventable, b: Eventable) => {
|
||||
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
|
||||
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
|
||||
return new Date(aTime).getTime() - new Date(bTime).getTime();
|
||||
});
|
||||
}
|
||||
return res.reduce((acc: Map<string, Eventable[]>, element: Eventable) => {
|
||||
const month = new Date(
|
||||
"role" in element ? element.event.beginsOn : element.beginsOn
|
||||
).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
});
|
||||
const filteredElements: Eventable[] = acc.get(month) || [];
|
||||
filteredElements.push(element);
|
||||
acc.set(month, filteredElements);
|
||||
return acc;
|
||||
}, new Map());
|
||||
};
|
||||
|
||||
loadMoreFutureParticipations(): void {
|
||||
this.futurePage += 1;
|
||||
if (this.$apollo.queries.futureParticipations) {
|
||||
this.$apollo.queries.futureParticipations.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
page: this.futurePage,
|
||||
limit: this.limit,
|
||||
},
|
||||
});
|
||||
}
|
||||
const monthlyFutureEvents = (): Map<string, Eventable[]> => {
|
||||
let eventable = [] as Eventable[];
|
||||
if (showAttending.value) {
|
||||
eventable = [...eventable, ...futureParticipations.value];
|
||||
}
|
||||
|
||||
loadMorePastParticipations(): void {
|
||||
this.pastPage += 1;
|
||||
if (this.$apollo.queries.pastParticipations) {
|
||||
this.$apollo.queries.pastParticipations.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
page: this.pastPage,
|
||||
limit: this.limit,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (showMyGroups.value) {
|
||||
eventable = [...eventable, ...groupEvents.value];
|
||||
}
|
||||
return monthlyEvents(eventable);
|
||||
};
|
||||
|
||||
eventDeleted(eventid: string): void {
|
||||
this.futureParticipations = this.futureParticipations.filter(
|
||||
const monthlyPastParticipations = computed((): Map<string, Eventable[]> => {
|
||||
return monthlyEvents(pastParticipations.value.elements, true);
|
||||
});
|
||||
|
||||
const monthParticipationsIds = (elements: Eventable[]): string[] => {
|
||||
const res = elements.filter((element: Eventable) => {
|
||||
return "role" in element;
|
||||
}) as IParticipant[];
|
||||
return res.map(({ event }: { event: IEvent }) => {
|
||||
return event.id as string;
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreFutureParticipations = (): void => {
|
||||
futurePage.value += 1;
|
||||
if (fetchMoreUpcomingEvents) {
|
||||
fetchMoreUpcomingEvents({
|
||||
// New variables
|
||||
variables: {
|
||||
page: futurePage.value,
|
||||
limit: limit.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadMorePastParticipations = (): void => {
|
||||
pastPage.value += 1;
|
||||
if (fetchMoreParticipations) {
|
||||
fetchMoreParticipations({
|
||||
// New variables
|
||||
variables: {
|
||||
page: pastPage.value,
|
||||
limit: limit.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const eventDeleted = (eventid: string): void => {
|
||||
futureParticipations.value = futureParticipations.value.filter(
|
||||
(participation) => participation.event.id !== eventid
|
||||
);
|
||||
pastParticipations.value = {
|
||||
elements: pastParticipations.value.elements.filter(
|
||||
(participation) => participation.event.id !== eventid
|
||||
);
|
||||
this.pastParticipations = {
|
||||
elements: this.pastParticipations.elements.filter(
|
||||
(participation) => participation.event.id !== eventid
|
||||
),
|
||||
total: this.pastParticipations.total - 1,
|
||||
};
|
||||
}
|
||||
),
|
||||
total: pastParticipations.value.total - 1,
|
||||
};
|
||||
};
|
||||
|
||||
get hideCreateEventButton(): boolean {
|
||||
return !!this.config?.restrictions?.onlyGroupsCanCreateEvents;
|
||||
}
|
||||
const { restrictions } = useRestrictions();
|
||||
|
||||
get firstDayOfWeek(): number {
|
||||
return this.$dateFnsLocale?.options?.weekStartsOn || 0;
|
||||
}
|
||||
}
|
||||
const hideCreateEventButton = computed((): boolean => {
|
||||
return restrictions.value?.onlyGroupsCanCreateEvents === true;
|
||||
});
|
||||
|
||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
||||
|
||||
const firstDayOfWeek = computed((): number => {
|
||||
return dateFnsLocale?.options?.weekStartsOn ?? 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
@import "~bulma/sass/utilities/mixins.sass";
|
||||
// @import "node_modules/bulma/sass/utilities/mixins.sass";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
// background: $white;
|
||||
|
||||
& > h1 {
|
||||
margin: 10px auto 5px;
|
||||
@@ -524,24 +452,18 @@ section {
|
||||
.not-found {
|
||||
margin-top: 2rem;
|
||||
.img-container {
|
||||
background-image: url("../../../public/img/pics/event_creation-480w.jpg");
|
||||
background-image: url("/img/pics/event_creation-480w.webp");
|
||||
@media (min-resolution: 2dppx) {
|
||||
& {
|
||||
background-image: url("../../../public/img/pics/event_creation-1024w.jpg");
|
||||
}
|
||||
}
|
||||
|
||||
&.webp {
|
||||
background-image: url("../../../public/img/pics/event_creation-480w.webp");
|
||||
@media (min-resolution: 2dppx) {
|
||||
& {
|
||||
background-image: url("../../../public/img/pics/event_creation-1024w.webp");
|
||||
}
|
||||
background-image: url("/img/pics/event_creation-1024w.webp");
|
||||
}
|
||||
}
|
||||
max-width: 450px;
|
||||
height: 300px;
|
||||
box-shadow: 0 0 8px 8px white inset;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 0 8px 8px #374151 inset;
|
||||
}
|
||||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
margin: auto auto 1rem;
|
||||
@@ -549,15 +471,15 @@ section {
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-areas: "filter" "events";
|
||||
align-items: start;
|
||||
// display: grid;
|
||||
// grid-template-areas: "filter" "events";
|
||||
// align-items: start;
|
||||
|
||||
@include desktop {
|
||||
gap: 2rem;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-template-areas: "filter events";
|
||||
}
|
||||
// // @include desktop {
|
||||
// gap: 2rem;
|
||||
// grid-template-columns: 1fr 3fr;
|
||||
// grid-template-areas: "filter events";
|
||||
// // }
|
||||
|
||||
.event-filter {
|
||||
grid-area: filter;
|
||||
@@ -565,18 +487,18 @@ section {
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem 1.25rem 0.25rem;
|
||||
|
||||
@include desktop {
|
||||
padding: 2rem 1.25rem;
|
||||
::v-deep .field.is-grouped {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
// @include desktop {
|
||||
// padding: 2rem 1.25rem;
|
||||
// :deep(.field.is-grouped) {
|
||||
// display: block;
|
||||
// }
|
||||
// }
|
||||
|
||||
::v-deep .field > .field {
|
||||
:deep(.field > .field) {
|
||||
margin: 0 auto 1.25rem !important;
|
||||
}
|
||||
|
||||
.date-filter ::v-deep .field-body {
|
||||
.date-filter :deep(.field-body) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,82 @@
|
||||
<template>
|
||||
<section class="section container" v-if="event">
|
||||
<section class="container mx-auto" v-if="event">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.MY_EVENTS, text: $t('My events') },
|
||||
{ name: RouteName.MY_EVENTS, text: t('My events') },
|
||||
{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: event.uuid },
|
||||
text: event.title,
|
||||
},
|
||||
{
|
||||
name: RouteName.PARTICIPANTS,
|
||||
name: RouteName.PARTICIPATIONS,
|
||||
params: { uuid: event.uuid },
|
||||
text: $t('Participants'),
|
||||
text: t('Participants'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<h1 class="title">{{ $t("Participants") }}</h1>
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<b-field :label="$t('Status')" horizontal label-for="role-select">
|
||||
<b-select v-model="role" id="role-select">
|
||||
<h1>{{ t("Participants") }}</h1>
|
||||
<div class="">
|
||||
<div class="">
|
||||
<div class="">
|
||||
<o-field :label="t('Status')" horizontal label-for="role-select">
|
||||
<o-select v-model="role" id="role-select">
|
||||
<option :value="null">
|
||||
{{ $t("Everything") }}
|
||||
{{ t("Everything") }}
|
||||
</option>
|
||||
<option :value="ParticipantRole.CREATOR">
|
||||
{{ $t("Organizer") }}
|
||||
{{ t("Organizer") }}
|
||||
</option>
|
||||
<option :value="ParticipantRole.PARTICIPANT">
|
||||
{{ $t("Participant") }}
|
||||
{{ t("Participant") }}
|
||||
</option>
|
||||
<option :value="ParticipantRole.NOT_APPROVED">
|
||||
{{ $t("Not approved") }}
|
||||
{{ t("Not approved") }}
|
||||
</option>
|
||||
<option :value="ParticipantRole.REJECTED">
|
||||
{{ $t("Rejected") }}
|
||||
{{ t("Rejected") }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</o-select>
|
||||
</o-field>
|
||||
</div>
|
||||
<div class="level-item" v-if="exportFormats.length > 0">
|
||||
<b-dropdown aria-role="list">
|
||||
<div class="" v-if="exportFormats.length > 0">
|
||||
<o-dropdown aria-role="list">
|
||||
<template #trigger="{ active }">
|
||||
<b-button
|
||||
:label="$t('Export')"
|
||||
type="is-primary"
|
||||
<o-button
|
||||
:label="t('Export')"
|
||||
variant="primary"
|
||||
:icon-right="active ? 'menu-up' : 'menu-down'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item
|
||||
<o-dropdown-item
|
||||
has-link
|
||||
v-for="format in exportFormats"
|
||||
:key="format"
|
||||
aria-role="listitem"
|
||||
@click="exportParticipants(format)"
|
||||
@keyup.enter="exportParticipants(format)"
|
||||
@click="
|
||||
exportParticipants({
|
||||
eventId: event?.id,
|
||||
format,
|
||||
})
|
||||
"
|
||||
@keyup.enter="
|
||||
exportParticipants({
|
||||
eventId: event.value?.id,
|
||||
format,
|
||||
})
|
||||
"
|
||||
>
|
||||
<button class="dropdown-button">
|
||||
<b-icon :icon="formatToIcon(format)"></b-icon>
|
||||
<o-icon :icon="formatToIcon(format)"></o-icon>
|
||||
{{ format }}
|
||||
</button>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-table
|
||||
<o-table
|
||||
:data="event.participants.elements"
|
||||
ref="queueTable"
|
||||
detailed
|
||||
@@ -76,26 +86,26 @@
|
||||
:is-row-checkable="(row) => row.role !== ParticipantRole.CREATOR"
|
||||
checkbox-position="left"
|
||||
:show-detail-icon="false"
|
||||
:loading="this.$apollo.loading"
|
||||
:loading="participantsLoading"
|
||||
paginated
|
||||
:current-page="page"
|
||||
backend-pagination
|
||||
:pagination-simple="true"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
:total="event.participants.total"
|
||||
:per-page="PARTICIPANTS_PER_PAGE"
|
||||
backend-sorting
|
||||
:default-sort-direction="'desc'"
|
||||
:default-sort="['insertedAt', 'desc']"
|
||||
@page-change="(newPage) => (page = newPage)"
|
||||
@sort="(field, order) => $emit('sort', field, order)"
|
||||
@sort="(field, order) => emit('sort', field, order)"
|
||||
>
|
||||
<b-table-column
|
||||
<o-table-column
|
||||
field="actor.preferredUsername"
|
||||
:label="$t('Participant')"
|
||||
:label="t('Participant')"
|
||||
v-slot="props"
|
||||
>
|
||||
<article class="media">
|
||||
@@ -105,20 +115,13 @@
|
||||
>
|
||||
<img class="is-rounded" :src="props.row.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon
|
||||
class="media-left"
|
||||
<Incognito
|
||||
v-else-if="props.row.actor.preferredUsername === 'anonymous'"
|
||||
size="is-large"
|
||||
icon="incognito"
|
||||
/>
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
:size="48"
|
||||
/>
|
||||
<AccountCircle v-else :size="48" />
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<div class="prose dark:prose-invert">
|
||||
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
|
||||
<span v-if="props.row.actor.name">{{
|
||||
props.row.actor.name
|
||||
@@ -129,42 +132,42 @@
|
||||
>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t("Anonymous participant") }}
|
||||
{{ t("Anonymous participant") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')" v-slot="props">
|
||||
</o-table-column>
|
||||
<o-table-column field="role" :label="t('Role')" v-slot="props">
|
||||
<b-tag
|
||||
type="is-primary"
|
||||
variant="primary"
|
||||
v-if="props.row.role === ParticipantRole.CREATOR"
|
||||
>
|
||||
{{ $t("Organizer") }}
|
||||
{{ t("Organizer") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
|
||||
{{ $t("Participant") }}
|
||||
{{ t("Participant") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === ParticipantRole.NOT_CONFIRMED">
|
||||
{{ $t("Not confirmed") }}
|
||||
{{ t("Not confirmed") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-warning"
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === ParticipantRole.NOT_APPROVED"
|
||||
>
|
||||
{{ $t("Not approved") }}
|
||||
{{ t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-danger"
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === ParticipantRole.REJECTED"
|
||||
>
|
||||
{{ $t("Rejected") }}
|
||||
{{ t("Rejected") }}
|
||||
</b-tag>
|
||||
</b-table-column>
|
||||
<b-table-column
|
||||
</o-table-column>
|
||||
<o-table-column
|
||||
field="metadata.message"
|
||||
class="column-message"
|
||||
:label="$t('Message')"
|
||||
:label="t('Message')"
|
||||
v-slot="props"
|
||||
>
|
||||
<div
|
||||
@@ -176,7 +179,7 @@
|
||||
v-if="props.row.metadata && props.row.metadata.message"
|
||||
>
|
||||
<p v-if="props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH">
|
||||
{{ props.row.metadata.message | ellipsize }}
|
||||
{{ ellipsize(props.row.metadata.message) }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ props.row.metadata.message }}
|
||||
@@ -188,67 +191,64 @@
|
||||
@click.stop="toggleQueueDetails(props.row)"
|
||||
>
|
||||
{{
|
||||
openDetailedRows[props.row.id] ? $t("View less") : $t("View more")
|
||||
openDetailedRows[props.row.id] ? t("View less") : t("View more")
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="has-text-grey-dark">
|
||||
{{ $t("No message") }}
|
||||
{{ t("No message") }}
|
||||
</p>
|
||||
</b-table-column>
|
||||
<b-table-column field="insertedAt" :label="$t('Date')" v-slot="props">
|
||||
<span class="has-text-centered">
|
||||
{{ props.row.insertedAt | formatDateString }}<br />{{
|
||||
props.row.insertedAt | formatTimeString
|
||||
</o-table-column>
|
||||
<o-table-column field="insertedAt" :label="t('Date')" v-slot="props">
|
||||
<span class="text-center">
|
||||
{{ formatDateString(props.row.insertedAt) }}<br />{{
|
||||
formatTimeString(props.row.insertedAt)
|
||||
}}
|
||||
</span>
|
||||
</b-table-column>
|
||||
</o-table-column>
|
||||
<template #detail="props">
|
||||
<article v-html="nl2br(props.row.metadata.message)" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<section class="section">
|
||||
<div class="content has-text-grey-dark has-text-centered">
|
||||
<p>{{ $t("No participant matches the filters") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<template #empty>
|
||||
<EmptyContent icon="account-circle" :inline="true">
|
||||
{{ t("No participant matches the filters") }}
|
||||
</EmptyContent>
|
||||
</template>
|
||||
<template slot="bottom-left">
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
<template #bottom-left>
|
||||
<div class="flex gap-2">
|
||||
<o-button
|
||||
@click="acceptParticipants(checkedRows)"
|
||||
type="is-success"
|
||||
variant="success"
|
||||
:disabled="!canAcceptParticipants"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
t(
|
||||
"No participant to approve|Approve participant|Approve {number} participants",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
{ number: checkedRows.length },
|
||||
checkedRows.length
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
<b-button
|
||||
</o-button>
|
||||
<o-button
|
||||
@click="refuseParticipants(checkedRows)"
|
||||
type="is-danger"
|
||||
variant="danger"
|
||||
:disabled="!canRefuseParticipants"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
t(
|
||||
"No participant to reject|Reject participant|Reject {number} participants",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
{ number: checkedRows.length },
|
||||
checkedRows.length
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</o-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { ParticipantRole } from "@/types/enums";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import { IEvent, IEventParticipantStats } from "../../types/event.model";
|
||||
@@ -257,263 +257,206 @@ import {
|
||||
PARTICIPANTS,
|
||||
UPDATE_PARTICIPANT,
|
||||
} from "../../graphql/event";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import { IPerson, usernameWithDomain } from "../../types/actor";
|
||||
import { EVENT_PARTICIPANTS } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { usernameWithDomain } from "../../types/actor";
|
||||
import { nl2br } from "../../utils/html";
|
||||
import { asyncForEach } from "../../utils/asyncForEach";
|
||||
import RouteName from "../../router/name";
|
||||
import VueRouter from "vue-router";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useParticipantsExportFormats } from "@/composition/config";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import {
|
||||
integerTransformer,
|
||||
enumTransformer,
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { formatDateString, formatTimeString } from "@/filters/datetime";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Incognito from "vue-material-design-icons/Incognito.vue";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 10;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
type exportFormat = "CSV" | "PDF" | "ODS";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: {
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
config: EVENT_PARTICIPANTS,
|
||||
event: {
|
||||
query: PARTICIPANTS,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: this.page,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: this.role,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
ellipsize: (text?: string) =>
|
||||
text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat("…"),
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Participants") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Participants extends Vue {
|
||||
@Prop({ required: true }) eventId!: string;
|
||||
const props = defineProps<{
|
||||
eventId: string;
|
||||
}>();
|
||||
|
||||
get page(): number {
|
||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
||||
}
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
set page(page: number) {
|
||||
this.pushRouter(RouteName.PARTICIPATIONS, {
|
||||
page: page.toString(),
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
const participantsExportFormats = useParticipantsExportFormats();
|
||||
|
||||
const ellipsize = (text?: string) =>
|
||||
text && text.substring(0, MESSAGE_ELLIPSIS_LENGTH).concat("…");
|
||||
|
||||
// metaInfo() {
|
||||
// return {
|
||||
// title: this.t("Participants") as string,
|
||||
// };
|
||||
// },
|
||||
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
const role = useRouteQuery(
|
||||
"role",
|
||||
ParticipantRole.PARTICIPANT,
|
||||
enumTransformer(ParticipantRole)
|
||||
);
|
||||
|
||||
const limit = ref(PARTICIPANTS_PER_PAGE);
|
||||
|
||||
const checkedRows = ref<IParticipant[]>([]);
|
||||
|
||||
// const queueTable = ref(null);
|
||||
|
||||
const { result: participantsResult, loading: participantsLoading } = useQuery<{
|
||||
event: IEvent;
|
||||
}>(
|
||||
PARTICIPANTS,
|
||||
() => ({
|
||||
uuid: props.eventId,
|
||||
page: page.value,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: role.value,
|
||||
}),
|
||||
() => ({
|
||||
enabled:
|
||||
currentActor.value?.id !== undefined &&
|
||||
page.value !== undefined &&
|
||||
role.value !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const event = computed(() => participantsResult.value?.event);
|
||||
|
||||
const participantStats = computed((): IEventParticipantStats | null => {
|
||||
if (!event.value) return null;
|
||||
return event.value.participantStats;
|
||||
});
|
||||
|
||||
const { mutate: updateParticipant, onError: onUpdateParticipantError } =
|
||||
useMutation(UPDATE_PARTICIPANT);
|
||||
|
||||
onUpdateParticipantError((e) => console.error(e));
|
||||
|
||||
const acceptParticipants = async (
|
||||
participants: IParticipant[]
|
||||
): Promise<void> => {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await updateParticipant({
|
||||
id: participant.id,
|
||||
role: ParticipantRole.PARTICIPANT,
|
||||
});
|
||||
}
|
||||
});
|
||||
checkedRows.value = [];
|
||||
};
|
||||
|
||||
get role(): ParticipantRole | null {
|
||||
if (
|
||||
Object.values(ParticipantRole).includes(
|
||||
this.$route.query.role as ParticipantRole
|
||||
)
|
||||
) {
|
||||
return this.$route.query.role as ParticipantRole;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set role(role: ParticipantRole | null) {
|
||||
this.pushRouter(RouteName.PARTICIPATIONS, {
|
||||
role: role || "",
|
||||
const refuseParticipants = async (
|
||||
participants: IParticipant[]
|
||||
): Promise<void> => {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await updateParticipant({
|
||||
id: participant.id,
|
||||
role: ParticipantRole.REJECTED,
|
||||
});
|
||||
});
|
||||
checkedRows.value = [];
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: exportParticipants,
|
||||
onDone: onExportParticipantsMutationDone,
|
||||
onError: onExportParticipantsMutationError,
|
||||
} = useMutation(EXPORT_EVENT_PARTICIPATIONS);
|
||||
|
||||
onExportParticipantsMutationDone(({ data }) => {
|
||||
const link =
|
||||
window.origin +
|
||||
"/exports/" +
|
||||
type.toLowerCase() +
|
||||
"/" +
|
||||
exportEventParticipants;
|
||||
console.log(link);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.href = link;
|
||||
a.setAttribute("download", "true");
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(a.href);
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
onExportParticipantsMutationError((e) => {
|
||||
console.error(e);
|
||||
if (e.graphQLErrors && e.graphQLErrors.length > 0) {
|
||||
notifier?.error(e.graphQLErrors[0].message);
|
||||
}
|
||||
});
|
||||
|
||||
limit = PARTICIPANTS_PER_PAGE;
|
||||
const exportFormats = computed((): exportFormat[] => {
|
||||
return (participantsExportFormats ?? []).map(
|
||||
(key) => key.toUpperCase() as exportFormat
|
||||
);
|
||||
});
|
||||
|
||||
event!: IEvent;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
ParticipantRole = ParticipantRole;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
PARTICIPANTS_PER_PAGE = PARTICIPANTS_PER_PAGE;
|
||||
|
||||
checkedRows: IParticipant[] = [];
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
@Ref("queueTable") readonly queueTable!: any;
|
||||
|
||||
get participantStats(): IEventParticipantStats | null {
|
||||
if (!this.event) return null;
|
||||
return this.event.participantStats;
|
||||
const formatToIcon = (format: exportFormat): string => {
|
||||
switch (format) {
|
||||
case "CSV":
|
||||
return "file-delimited";
|
||||
case "PDF":
|
||||
return "file-pdf-box";
|
||||
case "ODS":
|
||||
return "google-spreadsheet";
|
||||
}
|
||||
};
|
||||
|
||||
async acceptParticipant(participant: IParticipant): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_PARTICIPANT,
|
||||
variables: {
|
||||
id: participant.id,
|
||||
role: ParticipantRole.PARTICIPANT,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async refuseParticipant(participant: IParticipant): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_PARTICIPANT,
|
||||
variables: {
|
||||
id: participant.id,
|
||||
role: ParticipantRole.REJECTED,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async acceptParticipants(participants: IParticipant[]): Promise<void> {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.acceptParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async refuseParticipants(participants: IParticipant[]): Promise<void> {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.refuseParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async exportParticipants(type: exportFormat): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
data: { exportEventParticipants },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: EXPORT_EVENT_PARTICIPATIONS,
|
||||
variables: {
|
||||
eventId: this.event.id,
|
||||
format: type,
|
||||
},
|
||||
});
|
||||
const link =
|
||||
window.origin +
|
||||
"/exports/" +
|
||||
type.toLowerCase() +
|
||||
"/" +
|
||||
exportEventParticipants;
|
||||
console.log(link);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.href = link;
|
||||
a.setAttribute("download", "true");
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(a.href);
|
||||
document.body.removeChild(a);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
if (e.graphQLErrors && e.graphQLErrors.length > 0) {
|
||||
this.$notifier.error(e.graphQLErrors[0].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get exportFormats(): string[] {
|
||||
return (this.config?.exportFormats?.eventParticipants || []).map((key) =>
|
||||
key.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
formatToIcon(format: exportFormat): string {
|
||||
switch (format) {
|
||||
case "CSV":
|
||||
return "file-delimited";
|
||||
case "PDF":
|
||||
return "file-pdf-box";
|
||||
case "ODS":
|
||||
return "google-spreadsheet";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can accept participants if at least one of them is not approved
|
||||
*/
|
||||
get canAcceptParticipants(): boolean {
|
||||
return this.checkedRows.some((participant: IParticipant) =>
|
||||
[ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(
|
||||
participant.role
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* We can refuse participants if at least one of them is something different than not approved
|
||||
*/
|
||||
get canRefuseParticipants(): boolean {
|
||||
return this.checkedRows.some(
|
||||
(participant: IParticipant) =>
|
||||
participant.role !== ParticipantRole.REJECTED
|
||||
);
|
||||
}
|
||||
|
||||
MESSAGE_ELLIPSIS_LENGTH = MESSAGE_ELLIPSIS_LENGTH;
|
||||
|
||||
nl2br = nl2br;
|
||||
|
||||
toggleQueueDetails(row: IParticipant): void {
|
||||
if (
|
||||
row.metadata.message &&
|
||||
row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH
|
||||
/**
|
||||
* We can accept participants if at least one of them is not approved
|
||||
*/
|
||||
const canAcceptParticipants = (): boolean => {
|
||||
return checkedRows.value.some((participant: IParticipant) =>
|
||||
[ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(
|
||||
participant.role
|
||||
)
|
||||
return;
|
||||
this.queueTable.toggleDetails(row);
|
||||
if (row.id) {
|
||||
this.openDetailedRows[row.id] = !this.openDetailedRows[row.id];
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
openDetailedRows: Record<string, boolean> = {};
|
||||
/**
|
||||
* We can refuse participants if at least one of them is something different than not approved
|
||||
*/
|
||||
const canRefuseParticipants = (): boolean => {
|
||||
return checkedRows.value.some(
|
||||
(participant: IParticipant) => participant.role !== ParticipantRole.REJECTED
|
||||
);
|
||||
};
|
||||
|
||||
async pushRouter(
|
||||
routeName: string,
|
||||
args: Record<string, string>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: routeName,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
this.$apollo.queries.event.refetch();
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
const toggleQueueDetails = (row: IParticipant): void => {
|
||||
if (
|
||||
row.metadata.message &&
|
||||
row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH
|
||||
)
|
||||
return;
|
||||
queueTable.value.toggleDetails(row);
|
||||
if (row.id) {
|
||||
openDetailedRows.value[row.id] = !openDetailedRows.value[row.id];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openDetailedRows = <Record<string, boolean>>{};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
section.container.container {
|
||||
padding: 1rem;
|
||||
background: $white;
|
||||
// background: $white;
|
||||
}
|
||||
|
||||
.table {
|
||||
|
||||
Reference in New Issue
Block a user