Add anonymous and remote participations

This commit is contained in:
Thomas Citharel
2019-12-20 13:04:34 +01:00
parent 17e0b3968f
commit 2ed9050a90
135 changed files with 10141 additions and 2271 deletions

View File

@@ -8,117 +8,127 @@
{{ $t('Update event {name}', { name: event.title }) }}
</h1>
<div class="columns is-centered">
<form class="column is-two-thirds-desktop" ref="form">
<h2 class="subtitle">
{{ $t('General information') }}
</h2>
<picture-upload v-model="pictureFile" :textFallback="$t('Headline picture')" />
<form ref="form">
<h2 class="subtitle">
{{ $t('General information') }}
</h2>
<picture-upload v-model="pictureFile" :textFallback="$t('Headline picture')" />
<b-field :label="$t('Title')" :type="checkTitleLength[0]" :message="checkTitleLength[1]">
<b-input size="is-large" aria-required="true" required v-model="event.title" />
</b-field>
<b-field :label="$t('Title')" :type="checkTitleLength[0]" :message="checkTitleLength[1]">
<b-input size="is-large" aria-required="true" required v-model="event.title" />
</b-field>
<tag-input v-model="event.tags" :data="tags" path="title" />
<tag-input v-model="event.tags" :data="tags" path="title" />
<date-time-picker v-model="event.beginsOn" :label="$t('Starts on…')" />
<date-time-picker :min-datetime="event.beginsOn" v-model="event.endsOn" :label="$t('Ends on…')" />
<date-time-picker v-model="event.beginsOn" :label="$t('Starts on…')" />
<date-time-picker :min-datetime="event.beginsOn" v-model="event.endsOn" :label="$t('Ends on…')" />
<!-- <b-switch v-model="endsOnNull">{{ $t('No end date') }}</b-switch>-->
<b-button type="is-text" @click="dateSettingsIsOpen = true">{{ $t('Date parameters')}}</b-button>
<b-button type="is-text" @click="dateSettingsIsOpen = true">{{ $t('Date parameters')}}</b-button>
<address-auto-complete v-model="event.physicalAddress" />
<address-auto-complete v-model="event.physicalAddress" />
<b-field :label="$t('Organizer')">
<identity-picker-wrapper v-model="event.organizerActor" />
</b-field>
<b-field :label="$t('Organizer')">
<identity-picker-wrapper v-model="event.organizerActor" />
</b-field>
<div class="field">
<label class="label">{{ $t('Description') }}</label>
<editor v-model="event.description" />
</div>
<b-field :label="$t('Website / URL')">
<b-input icon="link" type="url" v-model="event.onlineAddress" placeholder="URL" />
</b-field>
<!--<b-field :label="$t('Category')">
<b-select placeholder="Select a category" v-model="event.category">
<option
v-for="category in categories"
:value="category"
:key="category"
>{{ $t(category) }}</option>
</b-select>
</b-field>-->
<h2 class="subtitle">
{{ $t('Who can view this event and participate') }}
</h2>
<div class="field">
<label class="label">{{ $t('Description') }}</label>
<editor v-model="event.description" />
</div>
<b-field :label="$t('Website / URL')">
<b-input icon="link" type="url" v-model="event.onlineAddress" placeholder="URL" />
</b-field>
<!--<b-field :label="$t('Category')">
<b-select placeholder="Select a category" v-model="event.category">
<option
v-for="category in categories"
:value="category"
:key="category"
>{{ $t(category) }}</option>
</b-select>
</b-field>-->
<h2 class="subtitle">
{{ $t('Who can view this event and participate') }}
</h2>
<div class="field">
<b-radio v-model="event.visibility"
name="eventVisibility"
:native-value="EventVisibility.PUBLIC">
{{ $t('Visible everywhere on the web (public)') }}
</b-radio>
</div>
<div class="field">
<b-radio v-model="event.visibility"
name="eventVisibility"
:native-value="EventVisibility.UNLISTED">
{{ $t('Only accessible through link and search (private)') }}
</b-radio>
</div>
<!-- <div class="field">
<b-radio v-model="event.visibility"
name="eventVisibility"
:native-value="EventVisibility.PRIVATE">
{{ $t('Page limited to my group (asks for auth)') }}
:native-value="EventVisibility.PUBLIC">
{{ $t('Visible everywhere on the web (public)') }}
</b-radio>
</div> -->
<div class="field">
<label class="label">{{ $t('Participation approval') }}</label>
<b-switch v-model="needsApproval">
{{ $t('I want to approve every participation request') }}
</b-switch>
</div>
<div class="field">
<label class="label">{{ $t('Number of places') }}</label>
<b-switch v-model="limitedPlaces">
{{ $t('Limited number of places') }}
</b-switch>
<b-radio v-model="event.visibility"
name="eventVisibility"
:native-value="EventVisibility.UNLISTED">
{{ $t('Only accessible through link and search (private)') }}
</b-radio>
</div>
<!-- <div class="field">
<b-radio v-model="event.visibility"
name="eventVisibility"
:native-value="EventVisibility.PRIVATE">
{{ $t('Page limited to my group (asks for auth)') }}
</b-radio>
</div> -->
<div class="box" v-if="limitedPlaces">
<b-field :label="$t('Number of places')">
<b-numberinput controls-position="compact" min="1" v-model="event.options.maximumAttendeeCapacity" />
</b-field>
<div class="field" v-if="config && config.anonymous.participation.allowed">
<label class="label">{{ $t('Anonymous participations') }}</label>
<b-switch v-model="event.options.anonymousParticipation">
{{ $t('I want to allow people to participate without an account.') }}
<small v-if="config.anonymous.participation.validation.email.confirmationRequired">
<br>
{{ $t('Anonymous participants will be asked to confirm their participation through e-mail.') }}
</small>
</b-switch>
</div>
<div class="field">
<label class="label">{{ $t('Participation approval') }}</label>
<b-switch v-model="needsApproval">
{{ $t('I want to approve every participation request') }}
</b-switch>
</div>
<div class="field">
<label class="label">{{ $t('Number of places') }}</label>
<b-switch v-model="limitedPlaces">
{{ $t('Limited number of places') }}
</b-switch>
</div>
<div class="box" v-if="limitedPlaces">
<b-field :label="$t('Number of places')">
<b-numberinput controls-position="compact" min="1" v-model="event.options.maximumAttendeeCapacity" />
</b-field>
<!--
<b-field>
<b-switch v-model="event.options.showRemainingAttendeeCapacity">
{{ $t('Show remaining number of places') }}
</b-switch>
</b-field>
<b-field>
<b-switch v-model="event.options.showRemainingAttendeeCapacity">
{{ $t('Show remaining number of places') }}
</b-switch>
</b-field>
<b-field>
<b-switch v-model="event.options.showParticipationPrice">
{{ $t('Display participation price') }}
</b-switch>
</b-field> -->
</div>
<b-field>
<b-switch v-model="event.options.showParticipationPrice">
{{ $t('Display participation price') }}
</b-switch>
</b-field> -->
</div>
<h2 class="subtitle">
{{ $t('Public comment moderation') }}
</h2>
<h2 class="subtitle">
{{ $t('Public comment moderation') }}
</h2>
<div class="field">
<b-radio v-model="event.options.commentModeration"
name="commentModeration"
:native-value="CommentModeration.ALLOW_ALL">
{{ $t('Allow all comments') }}
</b-radio>
</div>
<div class="field">
<b-radio v-model="event.options.commentModeration"
name="commentModeration"
:native-value="CommentModeration.ALLOW_ALL">
{{ $t('Allow all comments') }}
</b-radio>
</div>
<!-- <div class="field">-->
<!-- <b-radio v-model="event.options.commentModeration"-->
@@ -128,43 +138,42 @@
<!-- </b-radio>-->
<!-- </div>-->
<div class="field">
<b-radio v-model="event.options.commentModeration"
name="commentModeration"
:native-value="CommentModeration.CLOSED">
{{ $t('Close comments for all (except for admins)') }}
</b-radio>
</div>
<div class="field">
<b-radio v-model="event.options.commentModeration"
name="commentModeration"
:native-value="CommentModeration.CLOSED">
{{ $t('Close comments for all (except for admins)') }}
</b-radio>
</div>
<h2 class="subtitle">
{{ $t('Status') }}
</h2>
<h2 class="subtitle">
{{ $t('Status') }}
</h2>
<b-field>
<b-radio-button v-model="event.status"
name="status"
type="is-warning"
:native-value="EventStatus.TENTATIVE">
<b-icon icon="calendar-question" />
{{ $t('Tentative: Will be confirmed later') }}
</b-radio-button>
<b-radio-button v-model="event.status"
name="status"
type="is-success"
:native-value="EventStatus.CONFIRMED">
<b-icon icon="calendar-check" />
{{ $t('Confirmed: Will happen') }}
</b-radio-button>
<b-radio-button v-model="event.status"
name="status"
type="is-danger"
:native-value="EventStatus.CANCELLED">
<b-icon icon="calendar-remove" />
{{ $t("Cancelled: Won't happen") }}
</b-radio-button>
</b-field>
</form>
</div>
<b-field>
<b-radio-button v-model="event.status"
name="status"
type="is-warning"
:native-value="EventStatus.TENTATIVE">
<b-icon icon="calendar-question" />
{{ $t('Tentative: Will be confirmed later') }}
</b-radio-button>
<b-radio-button v-model="event.status"
name="status"
type="is-success"
:native-value="EventStatus.CONFIRMED">
<b-icon icon="calendar-check" />
{{ $t('Confirmed: Will happen') }}
</b-radio-button>
<b-radio-button v-model="event.status"
name="status"
type="is-danger"
:native-value="EventStatus.CANCELLED">
<b-icon icon="calendar-remove" />
{{ $t("Cancelled: Won't happen") }}
</b-radio-button>
</b-field>
</form>
</div>
<b-modal :active.sync="dateSettingsIsOpen" has-modal-card trap-focus>
<form action="">
@@ -227,6 +236,10 @@
<style lang="scss" scoped>
@import "@/variables.scss";
main section > .container {
background: $white;
}
h2.subtitle {
margin: 10px 0;
@@ -239,8 +252,7 @@
section {
& > .container {
margin-bottom: 2rem;
padding: 1rem;
padding: 2rem 1.5rem;
}
nav.navbar {
@@ -288,18 +300,17 @@ import { buildFileFromIPicture, buildFileVariable, readFileAsync } from '@/utils
import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
import { RouteName } from '@/router';
import 'intersection-observer';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
@Component({
components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor: EditorComponent },
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
tags: {
query: TAGS,
},
currentActor: CURRENT_ACTOR_CLIENT,
tags: TAGS,
config: CONFIG,
},
metaInfo() {
return {
@@ -319,6 +330,7 @@ export default class EditEvent extends Vue {
currentActor = new Person();
tags: ITag[] = [];
event: IEvent = new EventModel();
config!: IConfig;
unmodifiedEvent!: IEvent;
pictureFile: File | null = null;

View File

@@ -1,5 +1,3 @@
import {ParticipantRole} from "@/types/event.model";
import {ParticipantRole} from "@/types/event.model";
<template>
<div class="container">
<b-loading :active.sync="$apollo.loading" />
@@ -7,7 +5,7 @@ import {ParticipantRole} from "@/types/event.model";
<div>
<div class="header-picture" v-if="event.picture" :style="`background-image: url('${event.picture.url}')`" />
<div class="header-picture-default" v-else />
<section>
<section class="section">
<div class="title-and-participate-button">
<div class="title-wrapper">
<div class="date-component">
@@ -33,18 +31,39 @@ import {ParticipantRole} from "@/types/event.model";
<small v-if="event.options.maximumAttendeeCapacity">
{{ $tc('All the places have already been taken', numberOfPlacesStillAvailable, { places: numberOfPlacesStillAvailable}) }}
</small>
<b-tooltip type="is-dark" v-if="!event.local" :label="$t('The actual number of participants may differ, as this event is hosted on another instance.')">
<b-icon size="is-small" icon="help-circle-outline" />
</b-tooltip>
</span>
</div>
</div>
<div class="event-participation has-text-right" v-if="new Date(endDate) > new Date()">
<participation-button
v-if="currentActor.id && !actorIsOrganizer && !event.draft && (eventCapacityOK || actorIsParticipant) && event.status !== EventStatus.CANCELLED"
v-if="anonymousParticipation === null && (config.anonymous.participation.allowed || (currentActor.id && !actorIsOrganizer && !event.draft && (eventCapacityOK || actorIsParticipant) && event.status !== EventStatus.CANCELLED))"
:participation="participations[0]"
:event="event"
:current-actor="currentActor"
@joinEvent="joinEvent"
@joinModal="isJoinModalActive = true"
@confirmLeave="confirmLeave"
/>
<b-button type="is-text" v-if="anonymousParticipation !== null" @click="cancelAnonymousParticipation">{{ $t('Cancel anonymous participation')}}</b-button>
<small v-if="anonymousParticipation">
{{ $t('You are participating in this event anonymously')}}
<b-tooltip :label="$t('This information is saved only on your computer. Click for details')">
<router-link :to="{ name: RouteName.TERMS }">
<b-icon size="is-small" icon="help-circle-outline" />
</router-link>
</b-tooltip>
</small>
<small v-else-if="anonymousParticipation === false">
{{ $t("You are participating in this event anonymously but didn't confirm participation")}}
<b-tooltip :label="$t('This information is saved only on your computer. Click for details')">
<router-link :to="{ name: RouteName.TERMS }">
<b-icon size="is-small" icon="help-circle-outline" />
</router-link>
</b-tooltip>
</small>
</div>
<div v-else>
<button class="button is-primary" type="button" slot="trigger" disabled>
@@ -68,7 +87,9 @@ import {ParticipantRole} from "@/types/event.model";
<b-tag type="is-info" v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}</b-tag>
</span>
<span v-if="!event.local">
<b-tag type="is-primary">{{ event.organizerActor.domain }}</b-tag>
<a :href="event.url">
<b-tag type="is-primary">{{ event.organizerActor.domain }}</b-tag>
</a>
</span>
<router-link
v-if="event.tags && event.tags.length > 0"
@@ -165,7 +186,7 @@ import {ParticipantRole} from "@/types/event.model";
</div>
</div>
</section>
<div class="description" :class="{ exists: event.description }">
<section class="description section" :class="{ exists: event.description }">
<div class="description-container container">
<h3 class="title">
{{ $t('About this event') }}
@@ -178,14 +199,14 @@ import {ParticipantRole} from "@/types/event.model";
</div>
</div>
</div>
</div>
<section class="comments" ref="commentsObserver">
</section>
<section class="comments section" ref="commentsObserver">
<a href="#comments">
<h3 class="title" id="comments">{{ $t('Comments') }}</h3>
</a>
<comment-tree v-if="loadComments" :event="event" />
</section>
<section class="share" v-if="!event.draft">
<section class="share section" v-if="!event.draft">
<div class="container">
<div class="columns is-centered is-multiline">
<div class="column is-half-widescreen has-text-centered">
@@ -218,7 +239,7 @@ import {ParticipantRole} from "@/types/event.model";
</div>
</div>
</section>
<section class="more-events container" v-if="event.relatedEvents.length > 0">
<section class="more-events section container" v-if="event.relatedEvents.length > 0">
<h3 class="title has-text-centered">{{ $t('These events may interest you') }}</h3>
<div class="columns">
<div class="column is-one-third-desktop" v-for="relatedEvent in event.relatedEvents" :key="relatedEvent.uuid">
@@ -283,6 +304,14 @@ import { RouteName } from '@/router';
import { Address } from '@/types/address.model';
import CommentTree from '@/components/Comment/CommentTree.vue';
import 'intersection-observer';
import { CONFIG } from '@/graphql/config';
import {
AnonymousParticipationNotFoundError,
getLeaveTokenForParticipation,
isParticipatingInThisEvent,
removeAnonymousParticipation,
} from '@/services/AnonymousParticipationStorage';
import { IConfig } from '@/types/config.model';
@Component({
components: {
@@ -339,6 +368,7 @@ import 'intersection-observer';
return !this.currentActor || !this.event || !this.event.id || !this.currentActor.id;
},
},
config: CONFIG,
},
metaInfo() {
return {
@@ -360,6 +390,7 @@ export default class Event extends EventMixin {
event: IEvent = new EventModel();
currentActor!: IPerson;
identity: IPerson = new Person();
config!: IConfig;
participations: IParticipant[] = [];
oldParticipationRole!: String;
showMap: boolean = false;
@@ -370,6 +401,7 @@ export default class Event extends EventMixin {
RouteName = RouteName;
observer!: IntersectionObserver;
loadComments: boolean = false;
anonymousParticipation: boolean|null = null;
get eventTitle() {
if (!this.event) return undefined;
@@ -381,12 +413,22 @@ export default class Event extends EventMixin {
return this.event.description;
}
mounted() {
async mounted() {
this.identity = this.currentActor;
if (this.$route.hash.includes('#comment-')) {
this.loadComments = true;
}
try {
this.anonymousParticipation = await this.anonymousParticipationConfirmed();
} catch (e) {
if (e instanceof AnonymousParticipationNotFoundError) {
this.anonymousParticipation = null;
} else {
console.error(e);
}
}
this.observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry) {
@@ -529,63 +571,14 @@ export default class Event extends EventMixin {
cancelText: this.$t('Cancel') as string,
type: 'is-danger',
hasIcon: true,
onConfirm: () => this.leaveEvent(),
onConfirm: () => {
if (this.currentActor.id) {
this.leaveEvent(this.event, this.currentActor.id);
}
},
});
}
async leaveEvent() {
try {
const { data } = await this.$apollo.mutate<{ leaveEvent: IParticipant }>({
mutation: LEAVE_EVENT,
variables: {
eventId: this.event.id,
actorId: this.currentActor.id,
},
update: (store, { data }) => {
if (data == null) return;
const participationCachedData = store.readQuery<{ person: IPerson }>({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: this.event.id, actorId: this.currentActor.id },
});
if (participationCachedData == null) return;
const { person } = participationCachedData;
if (person === null) {
console.error('Cannot update participation cache, because of null value.');
return;
}
const participation = person.participations[0];
person.participations = [];
store.writeQuery({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: this.event.id, actorId: this.currentActor.id },
data: { person },
});
const eventCachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
if (eventCachedData == null) return;
const { event } = eventCachedData;
if (event === null) {
console.error('Cannot update event cache, because of null value.');
return;
}
if (participation.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.notApproved = event.participantStats.notApproved - 1;
} else {
event.participantStats.going = event.participantStats.going - 1;
event.participantStats.participant = event.participantStats.participant - 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
},
});
if (data) {
this.participationCancelledMessage();
}
} catch (error) {
console.error(error);
}
}
@Watch('participations')
watchParticipations() {
if (this.participations.length > 0) {
@@ -624,10 +617,6 @@ export default class Event extends EventMixin {
this.$notifier.info(this.$t('Your participation status has been changed') as string);
}
private participationCancelledMessage() {
this.$notifier.success(this.$t('You have cancelled your participation') as string);
}
async downloadIcsEvent() {
const data = await (await fetch(`${GRAPHQL_API_ENDPOINT}/events/${this.uuid}/export/ics`)).text();
const blob = new Blob([data], { type: 'text/calendar' });
@@ -709,11 +698,30 @@ export default class Event extends EventMixin {
if (!this.event.physicalAddress) return null;
return new Address(this.event.physicalAddress);
}
async anonymousParticipationConfirmed(): Promise<boolean> {
return await isParticipatingInThisEvent(this.uuid);
}
async cancelAnonymousParticipation() {
const token = await getLeaveTokenForParticipation(this.uuid) as String;
await this.leaveEvent(this.event, this.config.anonymous.actorId, token);
await removeAnonymousParticipation(this.uuid);
this.anonymousParticipation = null;
}
}
</script>
<style lang="scss" scoped>
@import "../../variables";
.section {
padding: 1rem 1.5rem;
}
main > .container {
background: $white;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
@@ -821,14 +829,14 @@ export default class Event extends EventMixin {
div.title-and-participate-button {
display: flex;
flex-wrap: wrap;
// flex-wrap: wrap;
/*flex-flow: row wrap;*/
justify-content: space-between;
/*align-self: center;*/
align-items: stretch;
/*align-content: space-around;*/
padding: 7.5px 10px 0;
margin-bottom: 1rem;
margin-bottom: 0.5rem;
div.title-wrapper {
display: flex;
@@ -906,7 +914,7 @@ export default class Event extends EventMixin {
p.tags {
span {
&.tag {
margin: 0 2px 4px;
margin: 0 2px;
&.is-success {
&::before {
@@ -919,7 +927,7 @@ export default class Event extends EventMixin {
margin: auto 5px;
}
margin-bottom: 1rem;
//margin-bottom: 1rem;
}
h3.title {
@@ -927,7 +935,7 @@ export default class Event extends EventMixin {
}
.description {
padding: 10px 0;
//padding: 10px 0;
min-height: 7rem;
&.exists {
@@ -942,8 +950,8 @@ export default class Event extends EventMixin {
background-image: url('../../assets/texting.svg');
}
}
border-top: solid 1px #111;
border-bottom: solid 1px #111;
border-top: solid 1px lighten($primary, 60%);
border-bottom: solid 1px lighten($primary, 60%);
.description-content {
/deep/ h1 {
@@ -990,8 +998,6 @@ export default class Event extends EventMixin {
}
.comments {
margin: 1rem auto 2rem;
a h3#comments {
margin-bottom: 5px;
}

View File

@@ -1,10 +1,10 @@
<template>
<div class="container">
<div class="section container">
<h1 class="title">{{ $t('Explore') }}</h1>
<section class="hero">
<div class="hero-body">
<form @submit.prevent="submit()">
<b-field :label="$t('Event')" grouped label-position="on-border">
<b-field :label="$t('Event')" grouped group-multiline label-position="on-border">
<b-input icon="magnify" type="search" size="is-large" expanded v-model="searchTerm" :placeholder="$t('For instance: London, Taekwondo, Architecture…')" />
<p class="control">
<b-button @click="submit" type="is-info" size="is-large" v-bind:disabled="searchTerm.trim().length === 0">{{ $t('Search') }}</b-button>
@@ -17,7 +17,7 @@
<b-loading :active.sync="$apollo.loading"></b-loading>
<h3 class="title">{{ $t('Featured events') }}</h3>
<div v-if="events.length > 0" class="columns is-multiline">
<div class="column is-one-quarter-desktop" v-for="event in events" :key="event.uuid">
<div class="column is-one-third-desktop" v-for="event in events" :key="event.uuid">
<EventCard
:event="event"
/>
@@ -66,6 +66,16 @@ export default class Explore extends Vue {
</script>
<style scoped lang="scss">
@import "@/variables.scss";
main > .container {
background: $white;
.hero-body {
padding: 1rem 1.5rem;
}
}
h1.title {
margin-top: 1.5rem;
}

View File

@@ -1,5 +1,5 @@
<template>
<main class="container">
<section class="section container">
<h1 class="title">
{{ $t('My events') }}
</h1>
@@ -10,7 +10,7 @@
</h2>
<transition-group name="list" tag="p">
<div v-for="month in monthlyFutureParticipations" :key="month[0]">
<h3>{{ month[0] }}</h3>
<h3 class="upcoming-month">{{ month[0] }}</h3>
<EventListCard
v-for="participation in month[1]"
:key="participation.id"
@@ -64,7 +64,7 @@
<b-message v-if="futureParticipations.length === 0 && pastParticipations.length === 0 && $apollo.loading === false" type="is-danger">
{{ $t('No events found') }}
</b-message>
</main>
</section>
</template>
<script lang="ts">
@@ -212,13 +212,15 @@ export default class MyEvents extends Vue {
<style lang="scss" scoped>
@import "../../variables";
main > .container {
background: $white;
}
.participation {
margin: 1rem auto;
}
section {
margin: 3rem auto;
& > h2 {
display: block;
color: $primary;
@@ -231,5 +233,9 @@ export default class MyEvents extends Vue {
margin-top: 2rem;
font-weight: bold;
}
.upcoming-month {
text-transform: capitalize;
}
}
</style>

View File

@@ -3,15 +3,19 @@
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
<b-tab-item>
<template slot="header">
<b-icon icon="account-multiple"></b-icon>
<b-icon icon="account-multiple" />
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
</template>
<template>
<section v-if="participantsAndCreators.length > 0">
<h2 class="title">{{ $t('Participants') }}</h2>
<p v-if="confirmedAnonymousParticipantsCountCount > 1">
{{ $tc('And no anonymous participations|And one anonymous participation|And {count} anonymous participations', confirmedAnonymousParticipantsCountCount, { count: confirmedAnonymousParticipantsCountCount}) }}
</p>
<div class="columns is-multiline">
<div class="column is-one-quarter-desktop" v-for="participant in participantsAndCreators" :key="participant.actor.id">
<participant-card
v-if="participant.actor.id !== config.anonymous.actorId"
:participant="participant"
:accept="acceptParticipant"
:reject="refuseParticipant"
@@ -24,7 +28,7 @@
</b-tab-item>
<b-tab-item :disabled="participantStats.notApproved === 0">
<template slot="header">
<b-icon icon="account-multiple-plus"></b-icon>
<b-icon icon="account-multiple-plus" />
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
</template>
<template>
@@ -75,7 +79,8 @@ import { PARTICIPANTS, UPDATE_PARTICIPANT } from '@/graphql/event';
import ParticipantCard from '@/components/Account/ParticipantCard.vue';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
@Component({
components: {
@@ -85,6 +90,7 @@ import { IPerson } from '@/types/actor';
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
config: CONFIG,
event: {
query: PARTICIPANTS,
variables() {
@@ -159,6 +165,7 @@ export default class Participants extends Vue {
queue: IParticipant[] = [];
rejected: IParticipant[] = [];
event!: IEvent;
config!: IConfig;
ParticipantRole = ParticipantRole;
currentActor!: IPerson;
@@ -179,6 +186,10 @@ export default class Participants extends Vue {
return [];
}
get confirmedAnonymousParticipantsCountCount(): number {
return this.participantsAndCreators.filter(({ actor: { id } }) => id === this.config.anonymous.actorId).length;
}
@Watch('participantStats', { deep: true })
watchParticipantStats(stats: IEventParticipantStats) {
if (!stats) return;