Various UI stuff (mainly implement mookup)

Fix lint

Disable modern mode

Fixes

UI fixes

Fixes

Ignore .po~ files

Fixes

Fix homepage

Fixes

Fixes

Mix format

Fix tests

Fix tests (yeah…)

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-04-03 17:29:03 +02:00
parent 2dcd65ea78
commit da2a0593ca
66 changed files with 14247 additions and 15872 deletions

View File

@@ -1,105 +1,101 @@
<template>
<section>
<div class="columns">
<div class="column">
<div class="card" v-if="person">
<div class="card-image" v-if="person.bannerUrl">
<figure class="image">
<img :src="person.bannerUrl">
<section class="container">
<div v-if="person">
<div class="card-image" v-if="person.bannerUrl">
<figure class="image">
<img :src="person.bannerUrl">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img :src="person.avatarUrl">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img :src="person.avatarUrl">
</figure>
</div>
<div class="media-content">
<p class="title">{{ person.name }}</p>
<p class="subtitle">@{{ person.preferredUsername }}</p>
</div>
</div>
<div class="content">
<p v-html="person.summary"></p>
</div>
<b-dropdown hoverable has-link aria-role="list">
<button class="button is-info" slot="trigger">
<translate>Public feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', true)">
<translate>Public RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', true)">
<translate>Public iCal Feed</translate>
</a>
</b-dropdown-item>
</b-dropdown>
<b-dropdown hoverable has-link aria-role="list" v-if="person.feedTokens.length > 0">
<button class="button is-info" slot="trigger">
<translate>Private feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', false)">
<translate>RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', false)">
<translate>iCal Feed</translate>
</a>
</b-dropdown-item>
</b-dropdown>
<a class="button" v-else @click="createToken">
<translate>Create token</translate>
</a>
<div class="media-content">
<p class="title">{{ person.name }}</p>
<p class="subtitle">@{{ person.preferredUsername }}</p>
</div>
<section v-if="person.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
</h2>
<div class="columns">
<EventCard
v-for="event in person.organizedEvents"
:event="event"
:options="{ hideDetails: true }"
:key="event.uuid"
class="column is-one-third"
/>
</div>
<div class="field is-grouped">
<p class="control">
<a
class="button"
@click="logoutUser()"
v-if="loggedPerson && loggedPerson.id === person.id"
>
<translate>User logout</translate>
</a>
</p>
<p class="control">
<a
class="button"
@click="deleteProfile()"
v-if="loggedPerson && loggedPerson.id === person.id"
>
<translate>Delete</translate>
</a>
</p>
</div>
</section>
</div>
<div class="content">
<vue-simple-markdown :source="person.summary"></vue-simple-markdown>
</div>
<b-dropdown hoverable has-link aria-role="list">
<button class="button is-primary" slot="trigger">
<translate>Public feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', true)">
<translate>Public RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', true)">
<translate>Public iCal Feed</translate>
</a>
</b-dropdown-item>
</b-dropdown>
<b-dropdown hoverable has-link aria-role="list" v-if="person.feedTokens.length > 0">
<button class="button is-info" slot="trigger">
<translate>Private feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', false)">
<translate>RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', false)">
<translate>iCal Feed</translate>
</a>
</b-dropdown-item>
</b-dropdown>
<a class="button" v-else-if="loggedPerson" @click="createToken">
<translate>Create token</translate>
</a>
</div>
<section v-if="person.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
</h2>
<div class="columns">
<EventCard
v-for="event in person.organizedEvents"
:event="event"
:options="{ hideDetails: true, organizerActor: person }"
:key="event.uuid"
class="column is-one-third"
/>
</div>
<div class="field is-grouped">
<p class="control">
<a
class="button"
@click="logoutUser()"
v-if="loggedPerson && loggedPerson.id === person.id"
>
<translate>User logout</translate>
</a>
</p>
<p class="control">
<a
class="button"
@click="deleteProfile()"
v-if="loggedPerson && loggedPerson.id === person.id"
>
<translate>Delete</translate>
</a>
</p>
</div>
</section>
</div>
</section>
</template>
@@ -172,3 +168,8 @@ export default class Profile extends Vue {
}
}
</script>
<style lang="scss">
@import "../../variables";
@import "~bulma/sass/utilities/_all";
@import "~bulma/sass/components/dropdown.sass";
</style>

View File

@@ -9,17 +9,17 @@
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { ErrorCode } from '@/types/error-code.model';
import { Component, Vue } from 'vue-property-decorator';
import { ErrorCode } from '@/types/error-code.model';
@Component
export default class ErrorPage extends Vue {
code: ErrorCode | null = null;
@Component
export default class ErrorPage extends Vue {
code: ErrorCode | null = null;
ErrorCode = ErrorCode;
ErrorCode = ErrorCode;
mounted() {
this.code = this.$route.query[ 'code' ] as ErrorCode;
}
mounted() {
this.code = this.$route.query['code'] as ErrorCode;
}
}
</script>

View File

@@ -95,7 +95,7 @@ export default class CreateEvent extends Vue {
});
})
.catch(error => {
console.log(error);
console.error(error);
});
}
}

View File

@@ -1,131 +1,258 @@
<template>
<div class="columns is-centered">
<div class="column is-three-quarters">
<b-loading :active.sync="$apollo.loading"></b-loading>
<div class="card" v-if="event">
<div class="card-image">
<figure class="image is-4by3">
<img src="https://picsum.photos/600/400/">
</figure>
</div>
<div class="card-content">
<span>{{ event.beginsOn | formatDay }}</span>
<span class="tag is-primary">{{ event.category }}</span>
<h1 class="title">{{ event.title }}</h1>
<router-link
:to="{name: 'Profile', params: { name: event.organizerActor.preferredUsername } }"
>
<figure v-if="event.organizerActor.avatarUrl">
<img :src="event.organizerActor.avatarUrl">
</figure>
</router-link>
<span
v-if="event.organizerActor"
>Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span>
<div class="field has-addons">
<p class="control">
<router-link
v-if="actorIsOrganizer()"
class="button"
:to="{ name: 'EditEvent', params: {uuid: event.uuid}}"
>
<translate>Edit</translate>
</router-link>
</p>
<p class="control">
<a class="button" @click="downloadIcsEvent()">
<translate>Download</translate>
<div>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="event">
<div class="header-picture container">
<figure class="image is-3by1">
<img src="https://picsum.photos/600/200/">
</figure>
</div>
<section class="container">
<div class="title-and-participate-button">
<div class="title-wrapper">
<div class="date-component">
<date-calendar-icon :date="event.beginsOn"></date-calendar-icon>
</div>
<h1 class="title">{{ event.title }}</h1>
</div>
<div v-if="!actorIsOrganizer()" class="participate-button has-text-centered">
<a v-if="!actorIsParticipant()" @click="joinEvent" class="button is-large is-primary is-rounded">
<b-icon icon="circle-outline"></b-icon>
<translate>Join</translate>
</a>
</p>
<p class="control">
<a class="button is-danger" v-if="actorIsOrganizer()" @click="deleteEvent()">
<translate>Delete</translate>
<a v-if="actorIsParticipant()" @click="leaveEvent" class="button is-large is-primary is-rounded">
<b-icon icon="check-circle"></b-icon>
<translate>Leave</translate>
</a>
</p>
</div>
<div>
<span>{{ event.beginsOn | formatDate }} - {{ event.endsOn | formatDate }}</span>
</div>
<div class="address" v-if="event.physicalAddress">
<h3 class="subtitle">Adresse</h3>
<address>
<span>{{ event.physicalAddress.description }}</span><br>
<span>{{ event.physicalAddress.floor }} {{ event.physicalAddress.street }}</span><br>
<span>{{ event.physicalAddress.postal_code }} {{ event.physicalAddress.locality }}</span><br>
<span>{{ event.physicalAddress.region }} {{ event.physicalAddress.country }}</span>
</address>
<div class="map">
<map-leaflet
:coords="event.physicalAddress.geom"
:popup="event.physicalAddress.description"
/>
</div>
</div>
<p v-if="actorIsOrganizer()">
<translate>You are an organizer.</translate>
</p>
<div v-else>
<p v-if="actorIsParticipant()">
<translate>You announced that you're going to this event.</translate>
</p>
<p v-else>
<translate>Are you going to this event?</translate><br />
<span>
<translate
:translate-n="event.participants.length"
translate-plural="%{event.participants.length} persons are going"
>
One person is going.
</translate>
</span>
</p>
</div>
<div v-if="!actorIsOrganizer()">
<a v-if="!actorIsParticipant()" @click="joinEvent" class="button">
<translate>Join</translate>
</a>
<a v-if="actorIsParticipant()" @click="leaveEvent" class="button">Leave</a>
</div>
<h2 class="subtitle">Details</h2>
<p v-if="event.description">
<vue-simple-markdown :source="event.description"></vue-simple-markdown>
</p>
<h2 class="subtitle">Participants</h2>
<span v-if="event.participants.length === 0">No participants yet.</span>
<div class="columns">
<router-link
class="card column"
v-for="participant in event.participants"
:key="participant.preferredUsername"
:to="{name: 'Profile', params: { name: participant.actor.preferredUsername }}"
>
<div>
<figure>
<img v-if="!participant.actor.avatarUrl" src="https://picsum.photos/125/125/">
<img v-else :src="participant.actor.avatarUrl">
</figure>
<span>{{ participant.actor.preferredUsername }}</span>
<div class="metadata columns">
<div class="column is-three-quarters-desktop">
<p class="tags" v-if="event.category || event.tags.length > 0">
<span class="tag" v-if="event.category">{{ event.category }}</span>
<span class="tag" v-if="event.tags" v-for="tag in event.tags">{{ tag.title }}</span>
<span class="visibility">
<translate v-if="event.visibility === EventVisibility.PUBLIC">public event</translate>
</span>
</p>
<div class="date-and-add-to-calendar">
<div class="date-and-privacy" v-if="event.beginsOn">
<b-icon icon="calendar-clock" />
<event-full-date :beginsOn="event.beginsOn" :endsOn="event.endsOn" />
</div>
<a class="add-to-calendar" @click="downloadIcsEvent()">
<b-icon icon="calendar-plus" />
<translate>Add to my calendar</translate>
</a>
</div>
</router-link>
<p class="slug">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
In aliquam libero quam, ut ultricies velit porttitor a. Maecenas mollis vestibulum dolor.
</p>
</div>
<div class="column sidebar">
<div class="field has-addons" v-if="actorIsOrganizer()">
<p class="control">
<router-link
class="button"
:to="{ name: 'EditEvent', params: {uuid: event.uuid}}"
>
<translate>Edit</translate>
</router-link>
</p>
<p class="control">
<a class="button is-danger" @click="deleteEvent()">
<translate>Delete</translate>
</a>
</p>
</div>
<div class="address-wrapper">
<b-icon icon="map" />
<translate v-if="!event.physicalAddress">No address defined</translate>
<div class="address" v-if="event.physicalAddress">
<address>
<span class="addressDescription">{{ event.physicalAddress.description }}</span>
<span>{{ event.physicalAddress.floor }} {{ event.physicalAddress.street }}</span>
<span>{{ event.physicalAddress.postal_code }} {{ event.physicalAddress.locality }}</span>
<!-- <span>{{ event.physicalAddress.region }} {{ event.physicalAddress.country }}</span>-->
</address>
<span class="map-show-button" @click="showMap = !showMap">
<translate>Show map</translate>
</span>
</div>
<!-- <div class="map" v-if="showMap">-->
<!-- <map-leaflet-->
<!-- :coords="event.physicalAddress.geom"-->
<!-- :popup="event.physicalAddress.description"-->
<!-- />-->
<!-- </div>-->
<b-modal v-if="event.physicalAddress" :active.sync="showMap" :width="800" scroll="keep">
<div class="map">
<map-leaflet
:coords="event.physicalAddress.geom"
:popup="event.physicalAddress.description"
/>
</div>
</b-modal>
</div>
<div class="organizer">
<router-link
:to="{name: 'Profile', params: { name: event.organizerActor.preferredUsername } }"
>
<translate
:translate-params="{name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}"
v-if="event.organizerActor">By %{ name }</translate>
<figure v-if="event.organizerActor.avatarUrl" class="image is-48x48">
<img
class="is-rounded"
:src="event.organizerActor.avatarUrl"
:alt="$gettextInterpolate('%{actor}\'s avatar', {actor: event.organizerActor.preferredUsername})" />
</figure>
</router-link>
</div>
</div>
</div>
</section>
<!-- <p v-if="actorIsOrganizer()">-->
<!-- <translate>You are an organizer.</translate>-->
<!-- </p>-->
<!-- <div v-else>-->
<!-- <p v-if="actorIsParticipant()">-->
<!-- <translate>You announced that you're going to this event.</translate>-->
<!-- </p>-->
<!-- <p v-else>-->
<!-- <translate>Are you going to this event?</translate><br />-->
<!-- <span>-->
<!-- <translate-->
<!-- :translate-n="event.participants.length"-->
<!-- translate-plural="%{event.participants.length} persons are going"-->
<!-- >-->
<!-- One person is going.-->
<!-- </translate>-->
<!-- </span>-->
<!-- </p>-->
<!-- </div>-->
<div class="description">
<div class="description-container container">
<h3 class="title">
<translate>About this event</translate>
</h3>
<p v-if="!event.description">
<translate>The event organizer didn't add any description.</translate>
</p>
<div class="columns" v-else="event.description">
<div class="column is-half">
<!-- <vue-simple-markdown :source="event.description" />-->
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse vehicula ex dapibus augue volutpat, ultrices cursus mi rutrum.
Nunc ante nunc, facilisis a tellus quis, tempor mollis diam. Aenean consectetur quis est a ultrices.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
<p><a href="https://framasoft.org">https://framasoft.org</a>
<p>
Nam sit amet est eget velit tristique commodo. Etiam sollicitudin dignissim diam, ut ultricies tortor.
Sed quis blandit diam, a tincidunt nunc. Donec tincidunt tristique neque at rhoncus. Ut eget vulputate felis.
Pellentesque nibh purus, viverra ac augue sed, iaculis feugiat velit. Nulla ut hendrerit elit.
Etiam at justo eu nunc tempus sagittis. Sed ac tincidunt tellus, sit amet luctus velit.
Nam ullamcorper eros eleifend, eleifend diam vitae, lobortis risus.
</p>
<p>
<em>
Curabitur rhoncus sapien tortor, vitae imperdiet massa scelerisque non.
Aliquam eu augue mi. Donec hendrerit lorem orci.
</em>
</p>
<p>
Donec volutpat, enim eu laoreet dictum, urna quam varius enim, eu convallis urna est vitae massa.
Morbi porttitor lacus a sem efficitur blandit. Mauris in est in quam tincidunt iaculis non vitae ipsum.
Phasellus eget velit tellus. Curabitur ac neque pharetra velit viverra mollis.
</p>
<img src="https://framasoft.org/img/biglogo-notxt.png" alt="logo Framasoft"/>
<p>Aenean gravida, ante vitae aliquet aliquet, elit quam tristique orci, sit amet dictum lorem ipsum nec tortor.
Vestibulum est eros, faucibus et semper vel, dapibus ac est. Suspendisse potenti. Suspendisse potenti.
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
Nulla molestie nisi ac risus hendrerit, dapibus mattis sapien scelerisque.
</p>
<p>Maecenas id pretium justo, nec dignissim sapien. Mauris in venenatis odio, in congue augue. </p>
</div>
</div>
</div>
</div>
<!-- <section class="container">-->
<!-- <h2 class="title">Participants</h2>-->
<!-- <span v-if="event.participants.length === 0">No participants yet.</span>-->
<!-- <div class="columns">-->
<!-- <router-link-->
<!-- class="column"-->
<!-- v-for="participant in event.participants"-->
<!-- :key="participant.preferredUsername"-->
<!-- :to="{name: 'Profile', params: { name: participant.actor.preferredUsername }}"-->
<!-- >-->
<!-- <div>-->
<!-- <figure>-->
<!-- <img v-if="!participant.actor.avatarUrl" src="https://picsum.photos/125/125/">-->
<!-- <img v-else :src="participant.actor.avatarUrl">-->
<!-- </figure>-->
<!-- <span>{{ participant.actor.preferredUsername }}</span>-->
<!-- </div>-->
<!-- </router-link>-->
<!-- </div>-->
<!-- </section>-->
<section class="share">
<div class="container">
<div class="columns">
<div class="column is-half has-text-centered">
<h3 class="title"><translate>Share this event</translate></h3>
<b-icon icon="mastodon" size="is-large" type="is-primary" />
<a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="facebook" size="is-large" type="is-primary" /></a>
<a :href="twitterShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="twitter" size="is-large" type="is-primary" /></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 -->
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="linkedin" size="is-large" type="is-primary" /></a>
</div>
<hr />
<div class="column is-half has-text-right add-to-calendar">
<h3 @click="downloadIcsEvent()">
<translate>Add to my calendar</translate>
</h3>
</div>
</div>
</div>
</section>
<section class="more-events container">
<h3 class="title has-text-centered"><translate>These events may interest you</translate></h3>
<div class="columns">
<div class="column" v-for="index in 3" :key="index">
<EventCard :event="event" />
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<script lang="ts">
import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { LOGGED_PERSON } from '@/graphql/actor';
import { IEvent, IParticipant } from '@/types/event.model';
import { EventVisibility, IEvent, IParticipant } from '@/types/event.model';
import { IPerson } from '@/types/actor.model';
import { RouteName } from '@/router';
import 'vue-simple-markdown/dist/vue-simple-markdown.css';
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';
@Component({
components: {
EventFullDate,
EventCard,
BIcon,
DateCalendarIcon,
'map-leaflet': () => import('@/components/Map.vue'),
},
apollo: {
@@ -148,6 +275,9 @@ export default class Event extends Vue {
event!: IEvent;
loggedPerson!: IPerson;
validationSent: boolean = false;
showMap: boolean = false;
EventVisibility = EventVisibility;
async deleteEvent() {
const router = this.$router;
@@ -241,12 +371,255 @@ export default class Event extends Vue {
return this.loggedPerson &&
this.loggedPerson.id === this.event.organizerActor.id;
}
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.event.description}&subject=${this.event.title}`;
}
}
</script>
<style lang="scss">
.address div.map {
height: 400px;
width: 400px;
padding: 25px 35px;
<style lang="scss" scoped>
@import "../../variables";
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.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%;
}
:not(.addressDescription) {
color: rgba(46, 62, 72, .6);
flex: 1;
min-width: 100%;
}
}
}
div.map {
height: 900px;
width: 100%;
padding: 25px 5px 0;
}
}
div.organizer {
display: inline-flex;
padding-top: 10px;
a {
color: #4a4a4a;
span {
line-height: 2.7rem;
padding-right: 6px;
}
}
}
}
div.title-and-participate-button {
display: flex;
flex-wrap: wrap;
/*flex-flow: row wrap;*/
justify-content: space-between;
/*align-self: center;*/
align-items: stretch;
/*align-content: space-around;*/
padding: 15px 10px 0;
div.title-wrapper {
display: flex;
flex: 1 1 auto;
div.date-component {
margin-right: 16px;
}
h1.title {
font-weight: normal;
word-break: break-word;
font-size: 1.7em;
}
}
.participate-button {
flex: 0 1 auto;
display: inline-flex;
a.button {
margin: 0 auto;
}
}
}
div.metadata {
padding: 0 10px;
div.date-and-add-to-calendar {
display: flex;
flex-wrap: wrap;
span.icon {
margin-right: 5px;
}
div.date-and-privacy {
color: $primary;
padding: 0.3rem;
background: $secondary;
font-weight: bold;
}
a.add-to-calendar {
flex: 0 0 auto;
margin-left: 10px;
color: #484849;
&:hover {
text-decoration: underline;
}
}
}
}
p.tags {
span {
&.tag {
&::before {
content: '#';
}
text-transform: uppercase;
}
&.visibility::before {
content: "⋅"
}
margin: auto 5px;
}
margin-bottom: 1rem;
}
h3.title {
font-size: 3rem;
font-weight: 300;
}
.description {
padding-top: 10px;
min-height: 40rem;
background-repeat: no-repeat;
background-size: 800px;
background-position: 95% 101%;
background-image: url('../../assets/texting.svg');
border-top: solid 1px #111;
border-bottom: solid 1px #111;
p {
margin: 10px auto;
a {
display: inline-block;
padding: 0.3rem;
background: $secondary;
color: #111;
}
}
}
.share {
border-bottom: solid 1px #111;
.columns {
& > * {
padding: 10rem 0;
}
.add-to-calendar {
background-repeat: no-repeat;
background-size: 400px;
background-position: 10% 50%;
background-image: url('../../assets/undraw_events.svg');
position: relative;
&::before {
content:"";
background: #B3B3B2;
position: absolute;
bottom: 25%;
left: 0;
height: 40%;
width: 1px;
}
h3 {
display: block;
color: $primary;
font-size: 3rem;
text-decoration: underline;
text-decoration-color: $secondary;
cursor: pointer;
max-width: 20rem;
margin-right: 0;
margin-left: auto;
}
}
}
}
.more-events {
margin: 50px auto;
}
</style>

View File

@@ -1,76 +1,73 @@
<template>
<section>
<div class="columns">
<div class="column">
<div class="card" v-if="group">
<div class="card-image" v-if="group.bannerUrl">
<figure class="image">
<img :src="group.bannerUrl">
<section class="container">
<div v-if="group">
<div class="card-image" v-if="group.bannerUrl">
<figure class="image">
<img :src="group.bannerUrl">
</figure>
</div>
<div class="box">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img :src="group.avatarUrl">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img :src="group.avatarUrl">
</figure>
</div>
<div class="media-content">
<p class="title">{{ group.name }}</p>
<p class="subtitle">@{{ group.preferredUsername }}</p>
</div>
</div>
<div class="content">
<p v-html="group.summary"></p>
</div>
<div class="media-content">
<p class="title">{{ group.name }}</p>
<p class="subtitle">@{{ group.preferredUsername }}</p>
</div>
<section v-if="group.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
</h2>
<div class="columns">
<EventCard
v-for="event in group.organizedEvents"
:event="event"
:options="{ hideDetails: true }"
:key="event.uuid"
class="column is-one-third"
/>
</div>
</section>
<section v-if="group.members.length > 0">
<h2 class="subtitle">
<translate>Members</translate>
</h2>
<div class="columns">
<span
v-for="member in group.members"
:key="member"
>{{ member.actor.preferredUsername }}</span>
</div>
</section>
</div>
<b-message v-if-else="!group && $apollo.loading === false" type="is-danger">
<translate>No group found</translate>
</b-message>
<div class="content">
<p v-html="group.summary"></p>
</div>
</div>
<section class="box" v-if="group.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
</h2>
<div class="columns">
<EventCard
v-for="event in group.organizedEvents"
:event="event"
:options="{ hideDetails: true }"
:key="event.uuid"
class="column is-one-third"
/>
</div>
</section>
<section v-if="group.members.length > 0">
<h2 class="subtitle">
<translate>Members</translate>
</h2>
<div class="columns">
<span
v-for="member in group.members"
:key="member.actor.preferredUsername"
>{{ member.actor.preferredUsername }}</span>
</div>
</section>
</div>
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
<translate>No group found</translate>
</b-message>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue';
import { FETCH_PERSON, LOGGED_PERSON } from '@/graphql/actor';
import { FETCH_GROUP, LOGGED_PERSON } from '@/graphql/actor';
import { IGroup } from '@/types/actor.model';
@Component({
apollo: {
person: {
query: FETCH_PERSON,
group: {
query: FETCH_GROUP,
variables() {
return {
name: this.$route.params.name,
name: this.$route.params.preferredUsername,
};
},
},
@@ -83,9 +80,9 @@ import { FETCH_PERSON, LOGGED_PERSON } from '@/graphql/actor';
},
})
export default class Group extends Vue {
@Prop({ type: String, required: true }) name!: string;
@Prop({ type: String, required: true }) preferredUsername!: string;
group = null;
group!: IGroup;
loading = true;
created() {
@@ -110,3 +107,8 @@ export default class Group extends Vue {
}
}
</script>
<style lang="scss">
section.container {
min-height: 30em;
}
</style>

View File

@@ -12,7 +12,7 @@
class="column is-one-quarter-desktop is-half-mobile"
/>
</div>
<router-link class="button" :to="{ name: 'CreateGroup' }">
<router-link class="button" :to="{ name: RouteName.CREATE_GROUP }">
<translate>Create group</translate>
</router-link>
</section>
@@ -27,6 +27,8 @@ export default class GroupList extends Vue {
groups = [];
loading = true;
RouteName = RouteName;
created() {
this.fetchData();
}

View File

@@ -3,11 +3,14 @@
<section class="hero is-link" v-if="!currentUser.id || !loggedPerson">
<div class="hero-body">
<div class="container">
<h1 class="title">Find events you like</h1>
<h2 class="subtitle">Share them with Mobilizon</h2>
<router-link class="button" :to="{ name: 'Register' }">
<h1 class="title">{{ config.name }}</h1>
<h2 class="subtitle">{{ config.description }}</h2>
<router-link class="button" :to="{ name: 'Register' }" v-if="config.registrationsOpen">
<translate>Register</translate>
</router-link>
<p v-else>
<translate>This instance isn't opened to registrations, but you can register on other instances.</translate>
</p>
</div>
</div>
</section>
@@ -18,8 +21,8 @@
>Welcome back %{username}</translate>
</h1>
</section>
<section v-if="loggedPerson">
<span class="events-nearby title">Events you're going at</span>
<section v-if="loggedPerson" class="container">
<span class="events-nearby title"><translate>Events you're going at</translate></span>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="goingToEvents.size > 0" v-for="row in Array.from(goingToEvents.entries())">
<!-- Iterators will be supported in v-for with VueJS 3 -->
@@ -62,16 +65,15 @@
<translate>You're not going to any event yet</translate>
</b-message>
</section>
<section>
<span class="events-nearby title">Events nearby you</span>
<section class="container">
<h3 class="events-nearby title"><translate>Events nearby you</translate></h3>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="events.length > 0" class="columns is-multiline">
<EventCard
v-for="event in events"
:key="event.uuid"
:event="event"
class="column is-one-quarter-desktop is-half-mobile"
/>
<div class="column is-one-third-desktop" v-for="event in events.slice(0, 6)" :key="event.uuid">
<EventCard
:event="event"
/>
</div>
</div>
<b-message v-else type="is-danger">
<translate>No events found</translate>
@@ -91,7 +93,9 @@ import { ICurrentUser } from '@/types/current-user.model';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { RouteName } from '@/router';
import { IEvent } from '@/types/event.model';
import DateComponent from '@/components/Event/Date.vue';
import DateComponent from '@/components/Event/DateCalendarIcon.vue';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
@Component({
apollo: {
@@ -105,6 +109,9 @@ import DateComponent from '@/components/Event/Date.vue';
currentUser: {
query: CURRENT_USER_CLIENT,
},
config: {
query: CONFIG,
},
},
components: {
DateComponent,
@@ -112,18 +119,19 @@ import DateComponent from '@/components/Event/Date.vue';
},
})
export default class Home extends Vue {
events = [];
events: Event[] = [];
locations = [];
city = { name: null };
country = { name: null };
loggedPerson: IPerson = new Person();
currentUser!: ICurrentUser;
config: IConfig = { description: '', name: '', registrationsOpen: false };
get displayed_name() {
return this.loggedPerson.name === null
? this.loggedPerson.preferredUsername
: this.loggedPerson.name;
}
// get displayed_name() {
// return this.loggedPerson && this.loggedPerson.name === null
// ? this.loggedPerson.preferredUsername
// : this.loggedPerson.name;
// }
isToday(date: string) {
return (new Date(date)).toDateString() === (new Date()).toDateString();
@@ -153,19 +161,18 @@ export default class Home extends Vue {
get goingToEvents(): Map<string, IEvent[]> {
const res = this.$data.loggedPerson.goingToEvents.filter((event) => {
return event.beginsOn != null && this.isBefore(event.beginsOn, 0)
return event.beginsOn != null && this.isBefore(event.beginsOn, 0);
});
res.sort(
(a: IEvent, b: IEvent) => new Date(a.beginsOn) > new Date(b.beginsOn),
);
const groups = res.reduce((acc: Map<string, IEvent[]>, event: IEvent) => {
return res.reduce((acc: Map<string, IEvent[]>, event: IEvent) => {
const day = (new Date(event.beginsOn)).toDateString();
const events: IEvent[] = acc.get(day) || [];
events.push(event);
acc.set(day, events);
return acc;
}, new Map());
return groups;
}, new Map());
}
geoLocalize() {
@@ -210,9 +217,9 @@ export default class Home extends Vue {
this.$router.push({ name: RouteName.EVENT, params: { uuid: event.uuid } });
}
ipLocation() {
return this.city.name ? this.city.name : this.country.name;
}
// ipLocation() {
// return this.city.name ? this.city.name : this.country.name;
// }
}
</script>

View File

@@ -1,8 +1,63 @@
<template>
<section>
<h1>
<translate>Page not found!</translate>
<img src="../assets/oh_no.jpg">
</h1>
<section class="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">
<h1 class="title">
<translate>The page you're looking for doesn't exist.</translate>
</h1>
<p>
<translate>Please make sure the address is correct and that the page hasn't been moved.</translate>
</p>
<p>
<translate>Please contact this instance's Mobilizon admin if you think this is a mistake.</translate>
</p>
<!-- The following should just be replaced with the SearchField component but it fails for some reason -->
<form @submit="enter">
<b-field class="search">
<b-input expanded icon="magnify" type="search" :placeholder="searchPlaceHolder" v-model="searchText" />
<p class="control">
<button type="submit" class="button is-primary"><translate>Search</translate></button>
</p>
</b-field>
</form>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import BField from 'buefy/src/components/field/Field.vue';
@Component({
components: {
BField,
},
})
export default class PageNotFound extends Vue {
searchText: string = '';
get searchPlaceHolder(): string {
return this.$gettext('Search events, groups, etc.');
}
enter() {
this.$router.push({ name: RouteName.SEARCH, params: { searchTerm: this.searchText } });
}
}
</script>
<style lang="scss">
.container.not-found {
margin: auto;
max-width: 600px;
img {
margin-top: 3rem;
}
p {
margin-bottom: 1em;
}
}
</style>

140
js/src/views/Search.vue Normal file
View File

@@ -0,0 +1,140 @@
<template>
<section class="container">
<h1>
<translate :translate-params="{ search: this.searchTerm }">Search results: « %{ search } »</translate>
</h1>
<b-loading :active.sync="$apollo.loading" />
<b-tabs v-model="activeTab" type="is-boxed" class="searchTabs" @change="changeTab">
<b-tab-item>
<template slot="header">
<b-icon icon="calendar"></b-icon>
<span><translate>Events</translate> <b-tag rounded>{{ events.length }}</b-tag> </span>
</template>
<div v-if="search.length > 0" class="columns is-multiline">
<div class="column is-one-quarter-desktop is-half-mobile"
v-for="event in events"
:key="event.uuid">
<EventCard
:event="event"
/>
</div>
</div>
<b-message v-else-if="$apollo.loading === false" type="is-danger">
<translate>No events found</translate>
</b-message>
</b-tab-item>
<b-tab-item>
<template slot="header">
<b-icon icon="account-multiple"></b-icon>
<span><translate>Groups</translate> <b-tag rounded>{{ groups.length }}</b-tag> </span>
</template>
<div v-if="groups.length > 0" class="columns is-multiline">
<div class="column is-one-quarter-desktop is-half-mobile"
v-for="group in groups"
:key="group.uuid">
<group-card :group="group" />
</div>
</div>
<b-message v-else-if="$apollo.loading === false" type="is-danger">
<translate>No groups found</translate>
</b-message>
</b-tab-item>
</b-tabs>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { SEARCH } from '@/graphql/search';
import { RouteName } from '@/router';
import { IEvent } from '@/types/event.model';
import { ISearch } from '@/types/search.model';
import EventCard from '@/components/Event/EventCard.vue';
import { IGroup, Group } from '@/types/actor.model';
import GroupCard from '@/components/Group/GroupCard.vue';
enum SearchTabs {
EVENTS = 0,
GROUPS = 1,
PERSONS = 2, // not used right now
}
const tabsName = {
events: SearchTabs.EVENTS,
groups: SearchTabs.GROUPS,
};
@Component({
apollo: {
search: {
query: SEARCH,
variables() {
return {
searchText: this.searchTerm,
};
},
skip() {
return !this.searchTerm;
},
},
},
components: {
GroupCard,
EventCard,
},
})
export default class Search extends Vue {
@Prop({ type: String, required: true }) searchTerm!: string;
@Prop({ type: String, required: false, default: 'events' }) searchType!: string;
search = [];
activeTab: SearchTabs = tabsName[this.searchType];
changeTab(index: number) {
switch (index) {
case SearchTabs.EVENTS:
this.$router.push({ name: RouteName.SEARCH, params: { searchTerm: this.searchTerm, searchType: 'events' } });
break;
case SearchTabs.GROUPS:
this.$router.push({ name: RouteName.SEARCH, params: { searchTerm: this.searchTerm, searchType: 'groups' } });
break;
}
}
@Watch('search')
changeTabForResult() {
if (this.events.length === 0 && this.groups.length > 0) {
this.activeTab = SearchTabs.GROUPS;
}
if (this.groups.length === 0 && this.events.length > 0) {
this.activeTab = SearchTabs.EVENTS;
}
}
@Watch('search')
@Watch('$route')
async loadSearch() {
await this.$apollo.queries['search'].refetch();
}
get events(): IEvent[] {
return this.search.filter((value: ISearch) => { return value.__typename === 'Event'; }) as IEvent[];
}
get groups(): IGroup[] {
const groups = this.search.filter((value: ISearch) => { return value.__typename === 'Group'; }) as IGroup[];
return groups.map(group => Object.assign(new Group(), group));
}
}
</script>
<style lang="scss">
@import "~bulma/sass/utilities/_all";
@import "~bulma/sass/components/tabs";
@import "~buefy/src/scss/components/tabs";
@import "~bulma/sass/elements/tag";
.searchTabs .tab-content {
background: #fff;
min-height: 10em;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="container">
<section class="hero">
<h1 class="title">
<translate>Welcome back!</translate>
@@ -12,14 +12,14 @@
<section v-if="!currentUser.isLoggedIn">
<div class="columns is-mobile is-centered">
<div class="column is-half card">
<div class="column is-half">
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<form @submit="loginAction">
<b-field label="Email">
<b-field :label="$gettext('Email')">
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
</b-field>
<b-field label="Password">
<b-field :label="$gettext('Password')">
<b-input
aria-required="true"
required
@@ -70,20 +70,20 @@ import { ILogin } from '@/types/login.model';
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { onLogin } from '@/vue-apollo';
import { RouteName } from '@/router';
import { LoginErrorCode } from '@/types/login-error-code.model'
import { ICurrentUser } from '@/types/current-user.model'
import { CONFIG } from '@/graphql/config'
import { IConfig } from '@/types/config.model'
import { LoginErrorCode } from '@/types/login-error-code.model';
import { ICurrentUser } from '@/types/current-user.model';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
@Component({
apollo: {
config: {
query: CONFIG
query: CONFIG,
},
currentUser: {
query: CURRENT_USER_CLIENT
}
}
query: CURRENT_USER_CLIENT,
},
},
})
export default class Login extends Vue {
@Prop({ type: String, required: false, default: '' }) email!: string;
@@ -113,9 +113,9 @@ export default class Login extends Vue {
this.credentials.email = this.email;
this.credentials.password = this.password;
let query = this.$route.query;
this.errorCode = query[ 'code' ] as LoginErrorCode;
this.redirect = query[ 'redirect' ] as string;
const query = this.$route.query;
this.errorCode = query['code'] as LoginErrorCode;
this.redirect = query['redirect'] as string;
}
async loginAction(e: Event) {
@@ -146,7 +146,7 @@ export default class Login extends Vue {
onLogin(this.$apollo);
if (this.redirect) {
this.$router.push(this.redirect)
this.$router.push(this.redirect);
} else {
this.$router.push({ name: RouteName.HOME });
}

View File

@@ -12,7 +12,7 @@
<div class="columns is-mobile">
<div class="column">
<div class="content">
<h2 class="subtitle" v-translate>Features</h2>
<h3 class="title" v-translate>Features</h3>
<ul>
<li v-translate>Create your communities and your events</li>
<li v-translate>Other stuff</li>
@@ -24,7 +24,7 @@
</p>
<hr>
<div class="content">
<h2 class="subtitle" v-translate>About this instance</h2>
<h3 class="title" v-translate>About this instance</h3>
<p>
<translate>Your local administrator resumed it's policy:</translate>
</p>
@@ -96,9 +96,7 @@
</form>
<div v-if="errors.length > 0">
<b-message type="is-danger" v-for="error in errors" :key="error">
<translate>{{ error }}</translate>
</b-message>
<b-message type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
</div>
</div>
</div>
@@ -154,6 +152,8 @@ export default class Register extends Vue {
</script>
<style lang="scss">
@import "../../variables";
.avatar-enter-active {
transition: opacity 1s ease;
}
@@ -166,4 +166,9 @@ export default class Register extends Vue {
.avatar-leave {
display: none;
}
h3.title {
background: $secondary;
display: inline;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<section class="columns">
<div class="column card">
<section class="container">
<div class="column">
<h1 class="title">
<translate>Resend confirmation email</translate>
</h1>

View File

@@ -1,6 +1,6 @@
<template>
<section class="columns">
<div class="card column">
<section class="container">
<div class="column">
<h1 class="title">
<translate>Password reset</translate>
</h1>