1150 lines
48 KiB
Vue
1150 lines
48 KiB
Vue
<template>
|
|
<div class="container">
|
|
<b-loading :active.sync="$apollo.loading"/>
|
|
<transition appear name="fade" mode="out-in">
|
|
<div>
|
|
<div class="header-picture" v-if="event.picture"
|
|
:style="`background-image: url('${event.picture.url}')`"/>
|
|
<div class="header-picture-default" v-else/>
|
|
<section class="section intro">
|
|
<div class="columns">
|
|
<div class="column is-1-tablet">
|
|
<date-calendar-icon :date="event.beginsOn"/>
|
|
</div>
|
|
<div class="column">
|
|
<h1 class="title" style="margin: 0">{{ event.title }}</h1>
|
|
<div class="organizer">
|
|
<span v-if="event.organizerActor && !event.attributedTo">
|
|
<span>
|
|
{{ $t('By @{username}', {username: event.organizerActor.preferredUsername}) }}
|
|
</span>
|
|
</span>
|
|
<span v-else-if="event.attributedTo && event.options.hideOrganizerWhenGroupEvent">
|
|
{{ $t('By @{group}', {username: event.organizerActor.preferredUsername, group: event.attributedTo.preferredUsername}) }}
|
|
</span>
|
|
<span v-else-if="event.organizerActor && event.attributedTo">
|
|
{{ $t('By @{username} and @{group}', {username: event.organizerActor.preferredUsername, group: event.attributedTo.preferredUsername}) }}
|
|
</span>
|
|
</div>
|
|
<p class="tags">
|
|
<router-link
|
|
v-if="event.tags && event.tags.length > 0"
|
|
v-for="tag in event.tags"
|
|
:key="tag.title"
|
|
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
|
>
|
|
<tag>{{ tag.title }}</tag>
|
|
</router-link>
|
|
</p>
|
|
</div>
|
|
<div class="column is-3-tablet">
|
|
<div>
|
|
<div class="event-participation has-text-right" v-if="new Date(endDate) > new Date()">
|
|
<participation-button
|
|
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"
|
|
@joinEventWithConfirmation="joinEventWithConfirmation"
|
|
@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>
|
|
<template>
|
|
<span>{{ $t('Event already passed')}}</span>
|
|
</template>
|
|
<b-icon icon="menu-down"/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="has-text-right">
|
|
<tag type="is-warning" size="is-medium" v-if="event.draft">{{ $t('Draft') }}</tag>
|
|
<span class="event-status" v-if="event.status !== EventStatus.CONFIRMED">
|
|
<tag type="is-warning" v-if="event.status === EventStatus.TENTATIVE">{{ $t('Event to be confirmed') }}</tag>
|
|
<tag type="is-danger"
|
|
v-if="event.status === EventStatus.CANCELLED">{{ $t('Event cancelled') }}</tag>
|
|
</span>
|
|
<template class="visibility" v-if="!event.draft">
|
|
<p v-if="event.visibility === EventVisibility.PUBLIC">{{ $t('Public event') }}
|
|
<b-icon icon="earth"/>
|
|
</p>
|
|
<p v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}
|
|
<b-icon icon="link"/>
|
|
</p>
|
|
</template>
|
|
<template v-if="!event.local">
|
|
<a :href="event.url">
|
|
<tag>{{ event.organizerActor.domain }}</tag>
|
|
</a>
|
|
</template>
|
|
<p>
|
|
<router-link v-if="actorIsOrganizer && event.draft === false"
|
|
:to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}">
|
|
<span v-if="event.options.maximumAttendeeCapacity">
|
|
{{ $tc('{going}/{capacity} available places', event.participantStats.going, {approved: event.participantStats.going, capacity: event.options.maximumAttendeeCapacity}) }}
|
|
</span>
|
|
<span v-else>
|
|
{{ $tc('No one is going to this event', event.participantStats.going, { going: event.participantStats.going}) }}
|
|
</span>
|
|
</router-link>
|
|
<span v-else>
|
|
<span v-if="event.options.maximumAttendeeCapacity">
|
|
{{ $tc('{going}/{capacity} available places', event.participantStats.going, {approved: event.participantStats.going, capacity: event.options.maximumAttendeeCapacity}) }}
|
|
</span>
|
|
<span v-else>
|
|
{{ $tc('No one is going to this event', event.participantStats.going, { going: event.participantStats.going}) }}
|
|
</span>
|
|
</span>
|
|
<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>
|
|
<b-icon icon="ticket-confirmation-outline"/>
|
|
</p>
|
|
<b-dropdown position="is-bottom-left" aria-role="list">
|
|
<span slot="trigger" role="button">
|
|
Actions <b-icon icon="dots-horizontal"/>
|
|
</span>
|
|
<b-dropdown-item aria-role="listitem" has-link
|
|
v-if="actorIsOrganizer || event.draft">
|
|
<router-link
|
|
:to="{ name: RouteName.EDIT_EVENT, params: {eventId: event.uuid}}"
|
|
>
|
|
{{ $t('Edit') }}
|
|
<b-icon icon="pencil"/>
|
|
</router-link>
|
|
</b-dropdown-item>
|
|
<b-dropdown-item aria-role="listitem" v-if="actorIsOrganizer || event.draft"
|
|
@click="openDeleteEventModalWrapper">
|
|
{{ $t('Delete') }}
|
|
<b-icon icon="delete"/>
|
|
</b-dropdown-item>
|
|
|
|
<hr class="dropdown-divider" aria-role="menuitem"
|
|
v-if="actorIsOrganizer || event.draft">
|
|
<b-dropdown-item has-link aria-role="listitem">
|
|
<a href="#share_section">
|
|
{{ $t('Share this event') }}
|
|
<b-icon icon="share"/>
|
|
</a>
|
|
</b-dropdown-item>
|
|
<b-dropdown-item aria-role="listitem" @click="downloadIcsEvent()"
|
|
v-if="!event.draft">
|
|
<span>
|
|
{{ $t('Add to my calendar') }}
|
|
<b-icon icon="calendar-plus"/>
|
|
</span>
|
|
</b-dropdown-item>
|
|
<b-dropdown-item aria-role="listitem">
|
|
<span @click="isReportModalActive = true">
|
|
{{ $t('Report') }}
|
|
<b-icon icon="flag"/>
|
|
</span>
|
|
</b-dropdown-item>
|
|
</b-dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<div class="event-description-wrapper">
|
|
<aside class="event-metadata">
|
|
<div class="sticky">
|
|
<event-metadata-block :title="$t('Location')"
|
|
:icon="physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'">
|
|
<div class="address-wrapper">
|
|
<span v-if="!physicalAddress">
|
|
{{ $t('No address defined') }}
|
|
</span>
|
|
<div class="address" v-if="physicalAddress">
|
|
<div>
|
|
<address>
|
|
<p class="addressDescription"
|
|
:title="physicalAddress.poiInfos.name">{{
|
|
physicalAddress.poiInfos.name }}</p>
|
|
<p>{{ physicalAddress.poiInfos.alternativeName }}</p>
|
|
</address>
|
|
</div>
|
|
<span class="map-show-button" @click="showMap = !showMap"
|
|
v-if="physicalAddress && physicalAddress.geom">
|
|
{{ $t('Show map') }}
|
|
</span>
|
|
</div>
|
|
<b-modal v-if="physicalAddress && physicalAddress.geom" :active.sync="showMap"
|
|
scroll="keep">
|
|
<div class="map">
|
|
<map-leaflet
|
|
:coords="physicalAddress.geom"
|
|
:marker="{ text: physicalAddress.fullName, icon: physicalAddress.poiInfos.poiIcon.icon }"
|
|
/>
|
|
</div>
|
|
</b-modal>
|
|
</div>
|
|
</event-metadata-block>
|
|
<event-metadata-block :title="$t('Date and time')" icon="calendar">
|
|
<event-full-date :beginsOn="event.beginsOn"
|
|
:show-start-time="event.options.showStartTime"
|
|
:show-end-time="event.options.showEndTime" :endsOn="event.endsOn"/>
|
|
</event-metadata-block>
|
|
<event-metadata-block :title="$t('Contact')">
|
|
<div class="media" style="align-items: center"
|
|
v-if="!event.attributedTo || !event.options.hideOrganizerWhenGroupEvent">
|
|
<div class="media-left">
|
|
<figure class="image is-32x32" v-if="event.organizerActor.avatar">
|
|
<img class="is-rounded" :src="event.organizerActor.avatar.url" alt=""/>
|
|
</figure>
|
|
<b-icon v-else size="is-medium" icon="account-circle"/>
|
|
</div>
|
|
|
|
<div class="media-content">
|
|
<p>{{ event.organizerActor.name ||
|
|
`@${event.organizerActor.preferredUsername}` }}</p>
|
|
<small class="has-text-grey" v-if="event.organizerActor.name">
|
|
@{{ event.organizerActor.preferredUsername }}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
<router-link v-if="event.attributedTo"
|
|
:to="{ name: RouteName.GROUP, params: { preferredUsername: event.attributedTo.preferredUsername } }">
|
|
<div class="media" style="align-items: center">
|
|
<div class="media-left">
|
|
<figure class="image is-32x32" v-if="event.attributedTo.avatar">
|
|
<img class="is-rounded" :src="event.attributedTo.avatar.url"
|
|
alt=""/>
|
|
</figure>
|
|
<b-icon v-else size="is-medium" icon="account-circle"/>
|
|
</div>
|
|
|
|
<div class="media-content">
|
|
<p>{{ event.attributedTo.name ||
|
|
`@${event.attributedTo.preferredUsername}` }}</p>
|
|
<p class="has-text-grey" v-if="event.attributedTo.name">
|
|
@{{ event.attributedTo.preferredUsername }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</router-link>
|
|
</event-metadata-block>
|
|
<event-metadata-block v-if="event.onlineAddress && urlToHostname(event.onlineAddress)" icon="link" :title="$t('Website')">
|
|
<a
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
:href="event.onlineAddress"
|
|
:title="$t('View page on {hostname} (in a new window)', {hostname: urlToHostname(event.onlineAddress) })"
|
|
>
|
|
{{ urlToHostname(event.onlineAddress) }}
|
|
</a>
|
|
</event-metadata-block>
|
|
</div>
|
|
</aside>
|
|
<div class="event-description-comments">
|
|
<section class="event-description">
|
|
<subtitle>
|
|
{{ $t('About this event') }}
|
|
</subtitle>
|
|
<p v-if="!event.description">
|
|
{{ $t("The event organizer didn't add any description.") }}
|
|
</p>
|
|
<div v-else>
|
|
<div class="description-content" ref="eventDescriptionElement"
|
|
v-html="event.description"/>
|
|
</div>
|
|
</section>
|
|
<section class="comments" ref="commentsObserver">
|
|
<a href="#comments">
|
|
<subtitle id="comments">{{ $t('Comments') }}</subtitle>
|
|
</a>
|
|
<comment-tree v-if="loadComments" :event="event"/>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<!-- <section class="share section" id="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">-->
|
|
<!-- <h3 class="title">{{ $t('Share this event') }}</h3>-->
|
|
<!-- <small class="maximumNumberOfPlacesWarning" v-if="!eventCapacityOK">-->
|
|
<!-- {{ $t('All the places have already been taken') }}-->
|
|
<!-- </small>-->
|
|
<!-- <div>-->
|
|
<!-- <!– <b-icon icon="mastodon" size="is-large" type="is-primary" />–>-->
|
|
|
|
<!-- <a :href="twitterShareUrl" target="_blank" rel="nofollow noopener">-->
|
|
<!-- <b-icon icon="twitter" size="is-large" type="is-primary"/>-->
|
|
<!-- </a>-->
|
|
<!-- <a :href="facebookShareUrl" target="_blank" rel="nofollow noopener">-->
|
|
<!-- <b-icon icon="facebook" size="is-large" type="is-primary"/>-->
|
|
<!-- </a>-->
|
|
<!-- <a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener">-->
|
|
<!-- <b-icon icon="linkedin" size="is-large" type="is-primary"/>-->
|
|
<!-- </a>-->
|
|
<!-- <a :href="diasporaShareUrl" class="diaspora" target="_blank"-->
|
|
<!-- rel="nofollow noopener">-->
|
|
<!-- <span data-v-5e15e80a="" class="icon has-text-primary is-large">-->
|
|
<!-- <img svg-inline src="../../assets/diaspora-icon.svg" alt="diaspora-logo"/>-->
|
|
<!-- </span>-->
|
|
<!-- </a>-->
|
|
<!-- <a :href="emailShareUrl" target="_blank" rel="nofollow noopener">-->
|
|
<!-- <b-icon icon="email" size="is-large" type="is-primary"/>-->
|
|
<!-- </a>-->
|
|
<!-- <!– TODO: mailto: links are not used anymore, we should provide a popup to redact a message instead –>-->
|
|
<!-- </div>-->
|
|
<!-- </div>-->
|
|
<!-- <hr/>-->
|
|
<!-- <div class="column is-half-widescreen has-text-right add-to-calendar">-->
|
|
<!-- <img src="../../assets/undraw_events.svg" class="is-hidden-mobile is-hidden-tablet-only"-->
|
|
<!-- alt=""/>-->
|
|
<!-- <h3 @click="downloadIcsEvent()">-->
|
|
<!-- {{ $t('Add to my calendar') }}-->
|
|
<!-- </h3>-->
|
|
<!-- </div>-->
|
|
<!-- </div>-->
|
|
<!-- </div>-->
|
|
<!-- </section>-->
|
|
<section class="more-events section" 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">
|
|
<EventCard :event="relatedEvent"/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<b-modal :active.sync="isReportModalActive" has-modal-card ref="reportModal">
|
|
<report-modal :on-confirm="reportEvent" :title="$t('Report this event')"
|
|
:outside-domain="event.organizerActor.domain" @close="$refs.reportModal.close()"/>
|
|
</b-modal>
|
|
<b-modal :active.sync="isJoinModalActive" has-modal-card ref="participationModal">
|
|
<identity-picker v-model="identity">
|
|
<template v-slot:footer>
|
|
<footer class="modal-card-foot">
|
|
<button
|
|
class="button"
|
|
ref="cancelButton"
|
|
@click="isJoinModalActive = false">
|
|
{{ $t('Cancel') }}
|
|
</button>
|
|
<button
|
|
class="button is-primary"
|
|
ref="confirmButton"
|
|
@click="event.joinOptions === EventJoinOptions.RESTRICTED ? joinEventWithConfirmation(identity) : joinEvent(identity)">
|
|
{{ $t('Confirm my particpation') }}
|
|
</button>
|
|
</footer>
|
|
</template>
|
|
</identity-picker>
|
|
</b-modal>
|
|
<b-modal :active.sync="isJoinConfirmationModalActive" has-modal-card ref="joinConfirmationModal">
|
|
<div class="modal-card">
|
|
<header class="modal-card-head">
|
|
<p class="modal-card-title">{{ $t('Participation confirmation')}}</p>
|
|
</header>
|
|
|
|
<section class="modal-card-body">
|
|
<p>{{ $t('The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?') }}</p>
|
|
<form @submit.prevent="joinEvent(actorForConfirmation, messageForConfirmation)">
|
|
<b-field :label="$t('Message')">
|
|
<b-input
|
|
type="textarea"
|
|
size="is-medium"
|
|
v-model="messageForConfirmation"
|
|
minlength="10">
|
|
</b-input>
|
|
</b-field>
|
|
<div class="buttons">
|
|
<b-button
|
|
native-type="button"
|
|
class="button"
|
|
ref="cancelButton"
|
|
@click="isJoinConfirmationModalActive = false">
|
|
{{ $t('Cancel') }}
|
|
</b-button>
|
|
<b-button type="is-primary" native-type="submit">{{ $t('Confirm my participation') }}
|
|
</b-button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
</b-modal>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import {
|
|
EVENT_PERSON_PARTICIPATION,
|
|
EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
|
FETCH_EVENT,
|
|
JOIN_EVENT,
|
|
} from '@/graphql/event';
|
|
import { Component, Prop, Watch } from 'vue-property-decorator';
|
|
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
|
import {
|
|
EventModel,
|
|
EventStatus,
|
|
EventVisibility,
|
|
IEvent,
|
|
IParticipant,
|
|
ParticipantRole,
|
|
EventJoinOptions,
|
|
} from '@/types/event.model';
|
|
import { IPerson, Person } from '@/types/actor';
|
|
import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';
|
|
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
|
|
import BIcon from 'buefy/src/components/icon/Icon.vue';
|
|
import EventCard from '@/components/Event/EventCard.vue';
|
|
import EventFullDate from '@/components/Event/EventFullDate.vue';
|
|
import ActorLink from '@/components/Account/ActorLink.vue';
|
|
import ReportModal from '@/components/Report/ReportModal.vue';
|
|
import { IReport } from '@/types/report.model';
|
|
import { CREATE_REPORT } from '@/graphql/report';
|
|
import EventMixin from '@/mixins/event';
|
|
import IdentityPicker from '@/views/Account/IdentityPicker.vue';
|
|
import ParticipationButton from '@/components/Event/ParticipationButton.vue';
|
|
import { GraphQLError } from 'graphql';
|
|
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';
|
|
import Subtitle from '@/components/Utils/Subtitle.vue';
|
|
import Tag from '@/components/Tag.vue';
|
|
import EventMetadataBlock from '@/components/Event/EventMetadataBlock.vue';
|
|
|
|
@Component({
|
|
components: {
|
|
EventMetadataBlock,
|
|
Subtitle,
|
|
ActorLink,
|
|
EventFullDate,
|
|
EventCard,
|
|
BIcon,
|
|
DateCalendarIcon,
|
|
ReportModal,
|
|
IdentityPicker,
|
|
ParticipationButton,
|
|
CommentTree,
|
|
Tag,
|
|
// tslint:disable:space-in-parens
|
|
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
|
|
// tslint:enable
|
|
},
|
|
apollo: {
|
|
event: {
|
|
query: FETCH_EVENT,
|
|
variables() {
|
|
return {
|
|
uuid: this.uuid,
|
|
};
|
|
},
|
|
error({ graphQLErrors }) {
|
|
this.handleErrors(graphQLErrors);
|
|
},
|
|
},
|
|
currentActor: {
|
|
query: CURRENT_ACTOR_CLIENT,
|
|
},
|
|
participations: {
|
|
query: EVENT_PERSON_PARTICIPATION,
|
|
variables() {
|
|
return {
|
|
eventId: this.event.id,
|
|
actorId: this.currentActor.id,
|
|
};
|
|
},
|
|
subscribeToMore: {
|
|
document: EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
|
variables() {
|
|
return {
|
|
eventId: this.event.id,
|
|
actorId: this.currentActor.id,
|
|
};
|
|
},
|
|
},
|
|
update: (data) => {
|
|
if (data && data.person) return data.person.participations;
|
|
return [];
|
|
},
|
|
skip() {
|
|
return !this.currentActor || !this.event || !this.event.id || !this.currentActor.id;
|
|
},
|
|
},
|
|
config: CONFIG,
|
|
},
|
|
metaInfo() {
|
|
return {
|
|
// if no subcomponents specify a metaInfo.title, this title will be used
|
|
// @ts-ignore
|
|
title: this.eventTitle,
|
|
// all titles will be injected into this template
|
|
titleTemplate: '%s | Mobilizon',
|
|
meta: [
|
|
// @ts-ignore
|
|
{ name: 'description', content: this.eventDescription },
|
|
],
|
|
};
|
|
},
|
|
})
|
|
export default class Event extends EventMixin {
|
|
@Prop({ type: String, required: true }) uuid!: string;
|
|
|
|
event: IEvent = new EventModel();
|
|
currentActor!: IPerson;
|
|
identity: IPerson = new Person();
|
|
config!: IConfig;
|
|
participations: IParticipant[] = [];
|
|
oldParticipationRole!: String;
|
|
showMap: boolean = false;
|
|
isReportModalActive: boolean = false;
|
|
isJoinModalActive: boolean = false;
|
|
isJoinConfirmationModalActive: boolean = false;
|
|
EventVisibility = EventVisibility;
|
|
EventStatus = EventStatus;
|
|
EventJoinOptions = EventJoinOptions;
|
|
RouteName = RouteName;
|
|
observer!: IntersectionObserver;
|
|
loadComments: boolean = false;
|
|
anonymousParticipation: boolean | null = null;
|
|
actorForConfirmation!: IPerson;
|
|
messageForConfirmation: string = '';
|
|
|
|
get eventTitle() {
|
|
if (!this.event) return undefined;
|
|
return this.event.title;
|
|
}
|
|
|
|
get eventDescription() {
|
|
if (!this.event) return undefined;
|
|
return this.event.description;
|
|
}
|
|
|
|
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) {
|
|
this.loadComments = entry.isIntersecting || this.loadComments;
|
|
}
|
|
}
|
|
}, {
|
|
rootMargin: '-50px 0px -50px',
|
|
});
|
|
this.observer.observe(this.$refs.commentsObserver as Element);
|
|
|
|
this.$watch('eventDescription', function (eventDescription) {
|
|
if (!eventDescription) return;
|
|
const eventDescriptionElement = this.$refs.eventDescriptionElement as HTMLElement;
|
|
|
|
eventDescriptionElement.addEventListener('click', ($event) => {
|
|
// TODO: Find the right type for target
|
|
let { target }: { target: any } = $event;
|
|
while (target && target.tagName !== 'A') target = target.parentNode;
|
|
// handle only links that occur inside the component and do not reference external resources
|
|
if (target && target.matches('.hashtag') && target.href) {
|
|
// some sanity checks taken from vue-router:
|
|
// https://github.com/vuejs/vue-router/blob/dev/src/components/link.js#L106
|
|
const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } = $event;
|
|
// don't handle with control keys
|
|
if (metaKey || altKey || ctrlKey || shiftKey) return;
|
|
// don't handle when preventDefault called
|
|
if (defaultPrevented) return;
|
|
// don't handle right clicks
|
|
if (button !== undefined && button !== 0) return;
|
|
// don't handle if `target="_blank"`
|
|
if (target && target.getAttribute) {
|
|
const linkTarget = target.getAttribute('target');
|
|
if (/\b_blank\b/i.test(linkTarget)) return;
|
|
}
|
|
// don't handle same page links/anchors
|
|
const url = new URL(target.href);
|
|
const to = url.pathname;
|
|
if (window.location.pathname !== to && $event.preventDefault) {
|
|
$event.preventDefault();
|
|
this.$router.push(to);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete the event, then redirect to home.
|
|
*/
|
|
async openDeleteEventModalWrapper() {
|
|
await this.openDeleteEventModal(this.event, this.currentActor);
|
|
}
|
|
|
|
async reportEvent(content: string, forward: boolean) {
|
|
this.isReportModalActive = false;
|
|
if (!this.event.organizerActor) return;
|
|
const eventTitle = this.event.title;
|
|
try {
|
|
await this.$apollo.mutate<IReport>({
|
|
mutation: CREATE_REPORT,
|
|
variables: {
|
|
eventId: this.event.id,
|
|
reporterId: this.currentActor.id,
|
|
reportedId: this.event.organizerActor.id,
|
|
content,
|
|
forward,
|
|
},
|
|
});
|
|
this.$notifier.success(this.$t('Event {eventTitle} reported', { eventTitle }) as string);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
joinEventWithConfirmation(actor: IPerson) {
|
|
this.isJoinConfirmationModalActive = true;
|
|
this.actorForConfirmation = actor;
|
|
}
|
|
|
|
async joinEvent(identity: IPerson, message: string | null = null) {
|
|
this.isJoinConfirmationModalActive = false;
|
|
this.isJoinModalActive = false;
|
|
try {
|
|
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
|
|
mutation: JOIN_EVENT,
|
|
variables: {
|
|
eventId: this.event.id,
|
|
actorId: identity.id,
|
|
message,
|
|
},
|
|
update: (store, { data }) => {
|
|
if (data == null) return;
|
|
|
|
const participationCachedData = store.readQuery<{ person: IPerson }>({
|
|
query: EVENT_PERSON_PARTICIPATION,
|
|
variables: { eventId: this.event.id, actorId: identity.id },
|
|
});
|
|
if (participationCachedData == null) return;
|
|
const { person } = participationCachedData;
|
|
if (person === null) {
|
|
console.error('Cannot update participation cache, because of null value.');
|
|
return;
|
|
}
|
|
person.participations.push(data.joinEvent);
|
|
store.writeQuery({
|
|
query: EVENT_PERSON_PARTICIPATION,
|
|
variables: { eventId: this.event.id, actorId: identity.id },
|
|
data: { person },
|
|
});
|
|
|
|
const cachedData = store.readQuery<{ event: IEvent }>({
|
|
query: FETCH_EVENT,
|
|
variables: { uuid: this.event.uuid },
|
|
});
|
|
if (cachedData == null) return;
|
|
const { event } = cachedData;
|
|
if (event === null) {
|
|
console.error('Cannot update event participant cache, because of null value.');
|
|
return;
|
|
}
|
|
|
|
if (data.joinEvent.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) {
|
|
if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
|
|
this.participationRequestedMessage();
|
|
} else {
|
|
this.participationConfirmedMessage();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
confirmLeave() {
|
|
this.$buefy.dialog.confirm({
|
|
title: this.$t('Leaving event "{title}"', { title: this.event.title }) as string,
|
|
message: this.$t('Are you sure you want to cancel your participation at event "{title}"?', { title: this.event.title }) as string,
|
|
confirmText: this.$t('Leave event') as string,
|
|
cancelText: this.$t('Cancel') as string,
|
|
type: 'is-danger',
|
|
hasIcon: true,
|
|
onConfirm: () => {
|
|
if (this.currentActor.id) {
|
|
this.leaveEvent(this.event, this.currentActor.id);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
@Watch('participations')
|
|
watchParticipations() {
|
|
if (this.participations.length > 0) {
|
|
if (this.oldParticipationRole
|
|
&& this.participations[0].role !== ParticipantRole.NOT_APPROVED
|
|
&& this.oldParticipationRole !== this.participations[0].role) {
|
|
switch (this.participations[0].role) {
|
|
case ParticipantRole.PARTICIPANT:
|
|
this.participationConfirmedMessage();
|
|
break;
|
|
case ParticipantRole.REJECTED:
|
|
this.participationRejectedMessage();
|
|
break;
|
|
default:
|
|
this.participationChangedMessage();
|
|
break;
|
|
}
|
|
}
|
|
this.oldParticipationRole = this.participations[0].role;
|
|
}
|
|
}
|
|
|
|
private participationConfirmedMessage() {
|
|
this.$notifier.success(this.$t('Your participation has been confirmed') as string);
|
|
}
|
|
|
|
private participationRequestedMessage() {
|
|
this.$notifier.success(this.$t('Your participation has been requested') as string);
|
|
}
|
|
|
|
private participationRejectedMessage() {
|
|
this.$notifier.error(this.$t('Your participation has been rejected') as string);
|
|
}
|
|
|
|
private participationChangedMessage() {
|
|
this.$notifier.info(this.$t('Your participation status has been changed') 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' });
|
|
const link = document.createElement('a');
|
|
link.href = window.URL.createObjectURL(blob);
|
|
link.download = `${this.event.title}.ics`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
async handleErrors(errors: GraphQLError) {
|
|
if (errors[0].message.includes('not found') || errors[0].message.includes('has invalid value $uuid')) {
|
|
await this.$router.push({ name: RouteName.PAGE_NOT_FOUND });
|
|
}
|
|
}
|
|
|
|
get actorIsParticipant() {
|
|
if (this.actorIsOrganizer) return true;
|
|
|
|
return this.participations.length > 0 && this.participations[0].role === ParticipantRole.PARTICIPANT;
|
|
}
|
|
|
|
get actorIsOrganizer() {
|
|
return this.participations.length > 0 && this.participations[0].role === ParticipantRole.CREATOR;
|
|
}
|
|
|
|
get endDate() {
|
|
return this.event.endsOn !== null && this.event.endsOn > this.event.beginsOn ? this.event.endsOn : this.event.beginsOn;
|
|
}
|
|
|
|
get twitterShareUrl(): string {
|
|
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${this.event.title}`;
|
|
}
|
|
|
|
get facebookShareUrl(): string {
|
|
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(this.event.url)}`;
|
|
}
|
|
|
|
get linkedInShareUrl(): string {
|
|
return `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(this.event.url)}&title=${this.event.title}`;
|
|
}
|
|
|
|
get emailShareUrl(): string {
|
|
return `mailto:?to=&body=${this.event.url}${encodeURIComponent('\n\n')}${this.textDescription}&subject=${this.event.title}`;
|
|
}
|
|
|
|
get diasporaShareUrl(): string {
|
|
return `https://share.diasporafoundation.org/?title=${encodeURIComponent(this.event.title)}&url=${encodeURIComponent(this.event.url)}`;
|
|
}
|
|
|
|
get textDescription(): string {
|
|
const meta = document.querySelector("meta[property='og:description']");
|
|
if (!meta) return '';
|
|
const desc = meta.getAttribute('content') || '';
|
|
return desc.substring(0, 1000);
|
|
}
|
|
|
|
get eventCapacityOK(): boolean {
|
|
if (this.event.draft) return true;
|
|
if (!this.event.options.maximumAttendeeCapacity) return true;
|
|
return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participant;
|
|
}
|
|
|
|
get numberOfPlacesStillAvailable(): number {
|
|
if (this.event.draft) return this.event.options.maximumAttendeeCapacity;
|
|
return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participant;
|
|
}
|
|
|
|
urlToHostname(url: string): string | null {
|
|
try {
|
|
return (new URL(url)).hostname;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
get physicalAddress(): Address | null {
|
|
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;
|
|
}
|
|
|
|
.fade-enter-active, .fade-leave-active {
|
|
transition: opacity .5s;
|
|
}
|
|
|
|
.fade-enter, .fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.header-picture, .header-picture-default {
|
|
height: 400px;
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
}
|
|
|
|
.header-picture-default {
|
|
background-image: url('/img/mobilizon_default_card.png');
|
|
}
|
|
|
|
div.sidebar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
flex-direction: column;
|
|
|
|
position: relative;
|
|
|
|
&::before {
|
|
content: "";
|
|
background: #B3B3B2;
|
|
position: absolute;
|
|
bottom: 30px;
|
|
top: 30px;
|
|
left: 0;
|
|
height: calc(100% - 60px);
|
|
width: 1px;
|
|
}
|
|
|
|
div.organizer {
|
|
display: inline-flex;
|
|
padding-top: 10px;
|
|
|
|
a {
|
|
color: #4a4a4a;
|
|
|
|
span {
|
|
line-height: 2.7rem;
|
|
padding-right: 6px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.intro.section {
|
|
background: white;
|
|
|
|
p.tags {
|
|
span {
|
|
&.tag {
|
|
margin: 0 2px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.event-description-wrapper {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
flex-direction: column;
|
|
padding: 0;
|
|
|
|
@media all and (min-width: 672px) {
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
& > aside, & > div {
|
|
@media all and (min-width: 672px) {
|
|
margin: 2rem auto;
|
|
}
|
|
}
|
|
|
|
aside.event-metadata {
|
|
min-width: 20rem;
|
|
flex: 1;
|
|
@media all and (min-width: 672px) {
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
.sticky {
|
|
position: sticky;
|
|
background: white;
|
|
top: 50px;
|
|
padding: 2rem;
|
|
}
|
|
|
|
div.address-wrapper {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-wrap: wrap;
|
|
|
|
div.address {
|
|
flex: 1;
|
|
|
|
.map-show-button {
|
|
cursor: pointer;
|
|
}
|
|
|
|
address {
|
|
font-style: normal;
|
|
flex-wrap: wrap;
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
|
|
span.addressDescription {
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
flex: 1 0 auto;
|
|
min-width: 100%;
|
|
max-width: 4rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:not(.addressDescription) {
|
|
color: rgba(46, 62, 72, .6);
|
|
flex: 1;
|
|
min-width: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
div.map {
|
|
height: 900px;
|
|
width: 100%;
|
|
padding: 25px 5px 0;
|
|
}
|
|
}
|
|
|
|
span.online-address {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
div.event-description-comments {
|
|
min-width: 20rem;
|
|
padding: 1rem;
|
|
flex: 2;
|
|
background: white;
|
|
}
|
|
|
|
.description-content {
|
|
/deep/ h1 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
/deep/ h2 {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
/deep/ h3 {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
/deep/ ul {
|
|
list-style-type: disc;
|
|
}
|
|
|
|
/deep/ li {
|
|
margin: 10px auto 10px 2rem;
|
|
}
|
|
|
|
/deep/ blockquote {
|
|
border-left: .2em solid #333;
|
|
display: block;
|
|
padding-left: 1em;
|
|
}
|
|
|
|
/deep/ p {
|
|
margin: 10px auto;
|
|
|
|
a {
|
|
display: inline-block;
|
|
padding: 0.3rem;
|
|
background: $secondary;
|
|
color: #111;
|
|
|
|
&:empty {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.comments {
|
|
padding-top: 3rem;
|
|
|
|
a h3#comments {
|
|
margin-bottom: 10px;
|
|
}
|
|
}
|
|
|
|
.share {
|
|
border-bottom: solid 1px $primary;
|
|
border-top: solid 1px $primary;
|
|
|
|
.diaspora span svg {
|
|
height: 2rem;
|
|
width: 2rem;
|
|
}
|
|
|
|
.columns {
|
|
|
|
& > * {
|
|
padding: 2rem 0;
|
|
}
|
|
|
|
h3 {
|
|
display: block;
|
|
color: $primary;
|
|
font-size: 3rem;
|
|
text-decoration: underline;
|
|
text-decoration-color: $secondary;
|
|
max-width: 20rem;
|
|
}
|
|
|
|
.column:first-child {
|
|
h3 {
|
|
margin: 0 auto 1rem;
|
|
font-weight: normal;
|
|
}
|
|
|
|
small.maximumNumberOfPlacesWarning {
|
|
margin: 0 auto 1rem;
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.column:last-child {
|
|
|
|
h3 {
|
|
margin-right: 0;
|
|
}
|
|
}
|
|
|
|
.add-to-calendar {
|
|
display: flex;
|
|
|
|
h3 {
|
|
margin-left: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
img {
|
|
max-width: 250px;
|
|
}
|
|
|
|
&::before {
|
|
content: "";
|
|
background: #B3B3B2;
|
|
position: absolute;
|
|
bottom: 25%;
|
|
height: 40%;
|
|
width: 1px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.more-events {
|
|
background: white;
|
|
}
|
|
|
|
.dropdown .dropdown-trigger span {
|
|
cursor: pointer;
|
|
}
|
|
|
|
a.dropdown-item, .dropdown .dropdown-menu .has-link a,
|
|
button.dropdown-item {
|
|
white-space: nowrap;
|
|
width: 100%;
|
|
padding-right: 1rem;
|
|
text-align: right;
|
|
}
|
|
</style>
|