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

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li class="is-active">

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<h1 class="title">{{ $t('Administration') }}</h1>
<div class="tile is-ancestor" v-if="dashboard">
<div class="tile is-vertical is-4">
@@ -45,6 +45,13 @@
</article>
</router-link>
</div>
<div class="tile is-parent">
<router-link :to="{ name: RouteName.ADMIN_SETTINGS }">
<article class="tile is-child box">
<p class="subtitle">{{ $t('Settings') }}</p>
</article>
</router-link>
</div>
</div>
<div class="tile is-parent">
<article class="tile is-child box">

View File

@@ -1,5 +1,5 @@
<template>
<div class="container">
<section class="section container">
<h1 class="title">{{ $t('Instances') }}</h1>
<div class="tabs is-boxed">
<ul>
@@ -18,7 +18,7 @@
</ul>
</div>
<router-view></router-view>
</div>
</section>
</template>
<script lang="ts">

View File

@@ -0,0 +1,94 @@
<template>
<section class="container section" v-if="adminSettings">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.DASHBOARD }">{{ $t('Dashboard') }}</router-link></li>
<li class="is-active"><router-link :to="{ name: RouteName.ADMIN_SETTINGS }" aria-current="page">{{ $t('Admin settings') }}</router-link></li>
</ul>
</nav>
<form @submit.prevent="updateSettings">
<b-field :label="$t('Instance Name')">
<b-input v-model="adminSettings.instanceName" />
</b-field>
<b-field :label="$t('Instance Description')">
<b-input type="textarea" v-model="adminSettings.instanceDescription" />
</b-field>
<b-field :label="$t('Allow registrations')">
<b-switch v-model="adminSettings.registrationsOpen">
<p class="content" v-if="adminSettings.registrationsOpen">{{ $t('Registration is allowed, anyone can register.')}}</p>
<p class="content" v-else>{{ $t('Registration is closed.')}}</p>
</b-switch>
</b-field>
<b-field :label="$t('Instance Terms Source')">
<div class="columns">
<div class="column is-one-quarter-desktop">
<b-field>
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.DEFAULT">{{ $t('Default Mobilizon.org terms')}}</b-radio>
</b-field>
<b-field>
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.URL">{{ $t('Custom URL')}}</b-radio>
</b-field>
<b-field>
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.CUSTOM">{{ $t('Custom text')}}</b-radio>
</b-field>
</div>
<div class="column">
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.DEFAULT">
<b>{{ $t('Default')}}</b>
<i18n tag="p" class="content" path="The {default_terms} will be used. They will be translated in the user's language.">
<a slot="default_terms" href="https://mobilizon.org/terms" target="_blank" rel="noopener">{{ $t('default Mobilizon terms')}}</a>
</i18n>
</div>
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.URL">
<b>{{ $t('URL')}}</b>
<p class="content">{{ $t("Set an URL to a page with your own terms.") }}</p>
</div>
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM">
<b>{{ $t('Custom')}}</b>
<p class="content">{{ $t("Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.") }}</p>
</div>
</div>
</div>
</b-field>
<b-field :label="$t('Instance Terms URL')" v-if="adminSettings.instanceTermsType === InstanceTermsType.URL">
<b-input type="URL" v-model="adminSettings.instanceTermsUrl" />
</b-field>
<b-field :label="$t('Instance Terms')" v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM">
<b-input type="textarea" v-model="adminSettings.instanceTerms" />
</b-field>
<b-button native-type="submit" type="is-primary">{{ $t('Save')}}</b-button>
</form>
</section>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS } from '@/graphql/admin';
import { IAdminSettings, InstanceTermsType } from '@/types/admin.model';
import { RouteName } from '@/router';
@Component({
apollo: {
adminSettings: ADMIN_SETTINGS,
},
})
export default class Settings extends Vue {
adminSettings!: IAdminSettings;
InstanceTermsType = InstanceTermsType;
RouteName = RouteName;
async updateSettings() {
try {
await this.$apollo.mutate({
mutation: SAVE_ADMIN_SETTINGS,
variables: {
...this.adminSettings,
},
});
this.$notifier.success(this.$t('Admin settings successfully saved.') as string);
} catch (e) {
console.error(e);
this.$notifier.error(this.$t('Failed to save admin settings') as string);
}
}
}
</script>

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;

View File

@@ -1,6 +1,6 @@
<template>
<div>
<section class="hero is-medium is-light is-bold" v-if="config && (!currentUser.id || !currentActor.id)">
<section class="hero is-light is-bold" v-if="config && (!currentUser.id || !currentActor.id)">
<div class="hero-body">
<div class="container">
<h1 class="title">{{ $t('Gather ⋅ Organize ⋅ Mobilize') }}</h1>
@@ -24,7 +24,7 @@
</div>
</div>
</section>
<div class="container" v-if="config">
<div class="container section" v-if="config">
<section v-if="currentActor.id">
<b-message type="is-info" v-if="welcomeBack">
{{ $t('Welcome back {username}!', { username: currentActor.displayName() }) }}
@@ -316,7 +316,15 @@ export default class Home extends Vue {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
@import "@/variables.scss";
@import "@/variables.scss";
main > div > .container {
background: $white;
}
.section {
padding: 1rem 1.5rem;
}
.search-autocomplete {
border: 1px solid #dbdbdb;
@@ -324,8 +332,6 @@ export default class Home extends Vue {
}
.events-featured {
margin: 25px auto;
.columns {
margin: 1rem auto 3rem;
}

57
js/src/views/Interact.vue Normal file
View File

@@ -0,0 +1,57 @@
<template>
<div class="container section">
<b-notification v-if="$apollo.queries.searchEvents.loading">{{ $t('Redirecting to event') }}</b-notification>
<b-notification v-if="$apollo.queries.searchEvents.skip" type="is-danger">{{ $t('Resource provided is not an URL') }}</b-notification>
</div>
</template>
<script lang="ts">
import {
Component,
Vue,
} from 'vue-property-decorator';
import { RouteName } from '@/router';
import { SEARCH_EVENTS } from '@/graphql/search';
import { IEvent } from '@/types/event.model';
@Component({
apollo: {
searchEvents: {
query: SEARCH_EVENTS,
variables() {
return {
searchText: this.$route.query.url,
};
},
skip() {
try {
const url = this.$route.query.url as string;
new URL(url);
return false;
} catch (e) {
if (e instanceof TypeError) {
return true;
}
}
},
async result({ data }) {
if (data.searchEvents && data.searchEvents.total > 0 && data.searchEvents.elements.length > 0) {
const event = data.searchEvents.elements[0];
return await this.$router.replace({ name: RouteName.EVENT, params: { uuid: event.uuid } });
}
},
},
},
})
export default class Interact extends Vue {
searchEvents!: IEvent[];
RouteName = RouteName;
}
</script>
<style lang="scss">
@import "@/variables.scss";
main > .container {
background: $white;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.DASHBOARD }">Dashboard</router-link></li>
@@ -9,7 +9,7 @@
<ul v-if="actionLogs.length > 0">
<li v-for="log in actionLogs">
<div class="box">
<img class="image" :src="log.actor.avatar.url" />
<img class="image" :src="log.actor.avatar.url" v-if="log.actor.avatar" />
<span>@{{ log.actor.preferredUsername }}</span>
<span v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED">
closed <router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link>

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="container section">
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<div class="container" v-if="report">
<nav class="breadcrumb" aria-label="breadcrumbs">
@@ -133,7 +133,8 @@
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
<p>{{ note.content }}</p>
<router-link :to="{ name: RouteName.PROFILE, params: { name: note.moderator.preferredUsername } }">
<img alt="" class="image" :src="note.moderator.avatar.url" /> @{{ note.moderator.preferredUsername }}
<img alt="" class="image" :src="note.moderator.avatar.url" v-if="note.moderator.avatar" />
@{{ note.moderator.preferredUsername }}
</router-link><br />
<small><a :href="`#note-${note.id}`" v-if="note.insertedAt">{{ note.insertedAt | formatDateTimeString }}</a></small>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="container section">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.DASHBOARD }">{{ $t('Dashboard') }}</router-link></li>

View File

@@ -1,5 +1,5 @@
<template>
<section class="container has-text-centered not-found">
<section class="section container has-text-centered not-found">
<div class="columns is-vertical">
<div class="column is-centered">
<img src="../assets/oh_no.jpg" alt="Not found 'oh no' picture">

64
js/src/views/Terms.vue Normal file
View File

@@ -0,0 +1,64 @@
<template>
<div class="container section">
<h2 class="title">{{ $t('Privacy Policy')}}</h2>
<div class="content" v-html="config.terms.bodyHtml" />
</div>
</template>
<script lang="ts">
import {
Component,
Vue, Watch,
} from 'vue-property-decorator';
import { TERMS } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { RouteName } from '@/router';
import { InstanceTermsType } from '@/types/admin.model';
@Component({
apollo: {
config: {
query: TERMS,
variables() {
return {
locale: this.locale,
};
},
skip() {
return !this.locale;
},
},
},
})
export default class Terms extends Vue {
config!: IConfig;
locale: string|null = null;
created() {
this.locale = this.$i18n.locale;
}
@Watch('config', { deep: true })
watchConfig(config: IConfig) {
if (config.terms.type) {
console.log(this.config.terms);
this.redirectToUrl();
}
}
redirectToUrl() {
if (this.config.terms.type === InstanceTermsType.URL) {
window.location.replace(this.config.terms.url);
}
}
RouteName = RouteName;
}
</script>
<style lang="scss">
@import "@/variables.scss";
main > .container {
background: $white;
}
</style>

View File

@@ -1,74 +1,68 @@
<template>
<div class="container">
<b-message v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN" title="Info" type="is-info">
{{ $t('You need to login.') }}
</b-message>
<section v-if="!currentUser.isLoggedIn">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">
{{ $t('Welcome back!') }}
</h1>
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
<span v-if="error === LoginError.USER_NOT_CONFIRMED">
<span>{{ $t("The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.") }}</span>
<i18n path="You may also ask to {resend_confirmation_email}.">
<router-link slot="resend_confirmation_email" :to="{ name: RouteName.RESEND_CONFIRMATION }">{{ $t('resend confirmation email') }}</router-link>
</i18n>
</span>
<span v-if="error === LoginError.USER_EMAIL_PASSWORD_INVALID">
{{ $t('Impossible to login, your email or password seems incorrect.') }}
</span>
<!-- TODO: Shouldn't we hide this information ? -->
<span v-if="error === LoginError.USER_DOES_NOT_EXIST">
{{ $t('No user account with this email was found. Maybe you made a typo?') }}
</span>
</b-message>
<form @submit="loginAction">
<b-field :label="$t('Email')">
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
</b-field>
<section class="section container" v-if="!currentUser.isLoggedIn">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">
{{ $t('Welcome back!') }}
</h1>
<b-message v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN" title="Info" type="is-info">
{{ $t('You need to login.') }}
</b-message>
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
<span v-if="error === LoginError.USER_NOT_CONFIRMED">
<span>{{ $t("The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.") }}</span>
<i18n path="You may also ask to {resend_confirmation_email}.">
<router-link slot="resend_confirmation_email" :to="{ name: RouteName.RESEND_CONFIRMATION }">{{ $t('resend confirmation email') }}</router-link>
</i18n>
</span>
<span v-if="error === LoginError.USER_EMAIL_PASSWORD_INVALID">
{{ $t('Impossible to login, your email or password seems incorrect.') }}
</span>
<!-- TODO: Shouldn't we hide this information ? -->
<span v-if="error === LoginError.USER_DOES_NOT_EXIST">
{{ $t('No user account with this email was found. Maybe you made a typo?') }}
</span>
</b-message>
<form @submit="loginAction">
<b-field :label="$t('Email')">
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
</b-field>
<b-field :label="$t('Password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
v-model="credentials.password"
/>
</b-field>
<b-field :label="$t('Password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
v-model="credentials.password"
/>
</b-field>
<p class="control has-text-centered">
<button class="button is-primary is-large">
{{ $t('Login') }}
</button>
</p>
<p class="control">
<router-link
class="button is-text"
:to="{ name: RouteName.SEND_PASSWORD_RESET, params: { email: credentials.email }}"
>
{{ $t('Forgot your password ?') }}
</router-link>
</p>
<p class="control" v-if="config && config.registrationsOpen">
<router-link
class="button is-text"
:to="{ name: RouteName.REGISTER, params: { default_email: credentials.email, default_password: credentials.password }}"
>
{{ $t('Register') }}
</router-link>
</p>
</form>
</div>
<p class="control has-text-centered">
<button class="button is-primary is-large">
{{ $t('Login') }}
</button>
</p>
<p class="control">
<router-link
class="button is-text"
:to="{ name: RouteName.SEND_PASSWORD_RESET, params: { email: credentials.email }}"
>
{{ $t('Forgot your password ?') }}
</router-link>
</p>
<p class="control" v-if="config && config.registrationsOpen">
<router-link
class="button is-text"
:to="{ name: RouteName.REGISTER, params: { default_email: credentials.email, default_password: credentials.password }}"
>
{{ $t('Register') }}
</router-link>
</p>
</form>
</div>
</section>
<b-message v-else title="Error" type="is-error">
{{ $t('You are already logged-in.') }}
</b-message>
</div>
</div>
</section>
</template>
<script lang="ts">

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.UPDATE_IDENTITY }">{{ $t('My account') }}</router-link></li>

View File

@@ -1,5 +1,5 @@
<template>
<section class="container columns is-mobile is-centered">
<section class="section container columns is-mobile is-centered">
<div class="card column is-half-desktop">
<h1>
{{ $t('Password reset') }}

View File

@@ -1,5 +1,5 @@
<template>
<div class="container">
<div class="section container">
<section class="hero">
<div class="hero-body">
<h1 class="title">

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">

View File

@@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<h1 class="title" v-if="loading">
{{ $t('Your account is being validated') }}
</h1>