217
js/src/views/Posts/Edit.vue
Normal file
217
js/src/views/Posts/Edit.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<form @submit.prevent="publish(false)">
|
||||
<div class="container section">
|
||||
<h1 class="title" v-if="isUpdate === true">
|
||||
{{ $t("Edit post") }}
|
||||
</h1>
|
||||
<h1 class="title" v-else>
|
||||
{{ $t("Add a new post") }}
|
||||
</h1>
|
||||
<subtitle>{{ $t("General information") }}</subtitle>
|
||||
<picture-upload v-model="pictureFile" :textFallback="$t('Headline picture')" />
|
||||
|
||||
<b-field :label="$t('Title')">
|
||||
<b-input size="is-large" aria-required="true" required v-model="post.title" />
|
||||
</b-field>
|
||||
|
||||
<tag-input v-model="post.tags" :data="tags" path="title" />
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t("Post") }}</label>
|
||||
<editor v-model="post.body" />
|
||||
</div>
|
||||
<subtitle>{{ $t("Who can view this post") }}</subtitle>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PUBLIC"
|
||||
>{{ $t("Visible everywhere on the web") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.UNLISTED"
|
||||
>{{ $t("Only accessible through link") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PRIVATE"
|
||||
>{{ $t("Only accessible to members of the group") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-text" @click="$router.go(-1)">{{ $t("Cancel") }}</b-button>
|
||||
</span>
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-danger is-outlined" @click="deletePost">{{
|
||||
$t("Delete post")
|
||||
}}</b-button>
|
||||
</span>
|
||||
<!-- If an post has been published we can't make it draft anymore -->
|
||||
<span class="navbar-item" v-if="post.draft === true">
|
||||
<b-button type="is-primary" outlined @click="publish(true)">{{
|
||||
$t("Save draft")
|
||||
}}</b-button>
|
||||
</span>
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
<span v-if="isUpdate === false || post.draft === true">{{ $t("Publish") }}</span>
|
||||
|
||||
<span v-else>{{ $t("Update post") }}</span>
|
||||
</b-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</form>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { CURRENT_ACTOR_CLIENT, FETCH_GROUP } from "../../graphql/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { FETCH_POST, CREATE_POST, UPDATE_POST, DELETE_POST } from "../../graphql/post";
|
||||
|
||||
import { IPost, PostVisibility } from "../../types/post.model";
|
||||
import Editor from "../../components/Editor.vue";
|
||||
import { IGroup } from "../../types/actor";
|
||||
import TagInput from "../../components/Event/TagInput.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
tags: TAGS,
|
||||
config: CONFIG,
|
||||
post: {
|
||||
query: FETCH_POST,
|
||||
variables() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.slug;
|
||||
},
|
||||
},
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Editor,
|
||||
TagInput,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
title: this.isUpdate
|
||||
? (this.$t("Edit post") as string)
|
||||
: (this.$t("Add a new post") as string),
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class EditPost extends Vue {
|
||||
@Prop({ required: false, type: String }) slug: undefined | string;
|
||||
|
||||
@Prop({ required: false, type: String }) preferredUsername!: string;
|
||||
|
||||
@Prop({ type: Boolean, default: false }) isUpdate!: boolean;
|
||||
|
||||
post: IPost = {
|
||||
title: "",
|
||||
body: "",
|
||||
local: true,
|
||||
draft: true,
|
||||
visibility: PostVisibility.PUBLIC,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
PostVisibility = PostVisibility;
|
||||
|
||||
async publish(draft: boolean) {
|
||||
if (this.isUpdate) {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UPDATE_POST,
|
||||
variables: {
|
||||
id: this.post.id,
|
||||
title: this.post.title,
|
||||
body: this.post.body,
|
||||
tags: (this.post.tags || []).map(({ title }) => title),
|
||||
visibility: this.post.visibility,
|
||||
draft,
|
||||
},
|
||||
});
|
||||
if (data && data.updatePost) {
|
||||
return this.$router.push({ name: RouteName.POST, params: { slug: data.updatePost.slug } });
|
||||
}
|
||||
} else {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: CREATE_POST,
|
||||
variables: {
|
||||
...this.post,
|
||||
tags: (this.post.tags || []).map(({ title }) => title),
|
||||
attributedToId: this.group.id,
|
||||
draft,
|
||||
},
|
||||
});
|
||||
if (data && data.createPost) {
|
||||
return this.$router.push({ name: RouteName.POST, params: { slug: data.createPost.slug } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deletePost() {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: DELETE_POST,
|
||||
variables: {
|
||||
id: this.post.id,
|
||||
},
|
||||
});
|
||||
if (data && this.post.attributedTo) {
|
||||
return this.$router.push({
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: this.post.attributedTo.preferredUsername },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
form {
|
||||
nav.navbar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
min-height: 2rem;
|
||||
|
||||
.container {
|
||||
min-height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
js/src/views/Posts/List.vue
Normal file
86
js/src/views/Posts/List.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="section container">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="group">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-if="group"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ group.name || group.preferredUsername }}</router-link
|
||||
>
|
||||
<b-skeleton v-else :animated="true"></b-skeleton>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
v-if="group"
|
||||
:to="{
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Posts") }}</router-link
|
||||
>
|
||||
<b-skeleton v-else :animated="true"></b-skeleton>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div v-if="group">
|
||||
<router-link
|
||||
v-for="post in group.posts.elements"
|
||||
:key="post.id"
|
||||
:to="{ name: RouteName.POST, params: { slug: post.slug } }"
|
||||
>
|
||||
{{ post.title }}
|
||||
</router-link>
|
||||
</div>
|
||||
<b-skeleton v-else :animated="true"></b-skeleton>
|
||||
</section>
|
||||
<pre>{{ group }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { FETCH_GROUP_POSTS } from "../../graphql/post";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
import { IPost } from "../../types/post.model";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP_POSTS,
|
||||
variables() {
|
||||
return {
|
||||
preferredUsername: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
// update(data) {
|
||||
// console.log(data);
|
||||
// return data.group.posts;
|
||||
// },
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class PostList extends Vue {
|
||||
@Prop({ required: true, type: String }) preferredUsername!: string;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
posts!: Paginate<IPost>;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
}
|
||||
</script>
|
||||
184
js/src/views/Posts/Post.vue
Normal file
184
js/src/views/Posts/Post.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div>
|
||||
<article class="container" v-if="post">
|
||||
<section class="heading-section">
|
||||
<h1 class="title">{{ post.title }}</h1>
|
||||
<i18n tag="span" path="By {author}" class="authors">
|
||||
<router-link
|
||||
slot="author"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(post.attributedTo) },
|
||||
}"
|
||||
>{{ post.attributedTo.name }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
<p class="published" v-if="!post.draft">{{ post.publishAt | formatDateTimeString }}</p>
|
||||
<p class="buttons" v-if="isCurrentActorMember">
|
||||
<b-tag type="is-warning" size="is-medium" v-if="post.draft">{{ $t("Draft") }}</b-tag>
|
||||
<router-link
|
||||
:to="{ name: RouteName.POST_EDIT, params: { slug: post.slug } }"
|
||||
tag="button"
|
||||
class="button is-text"
|
||||
>{{ $t("Edit") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
<section v-html="post.body" class="content" />
|
||||
<section class="tags">
|
||||
<router-link
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.title"
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
>
|
||||
<tag>{{ tag.title }}</tag>
|
||||
</router-link>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import Editor from "@/components/Editor.vue";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS, FETCH_GROUP } from "../../graphql/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { FETCH_POST, CREATE_POST } from "../../graphql/post";
|
||||
|
||||
import { IPost, PostVisibility } from "../../types/post.model";
|
||||
import { IGroup, IMember, usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import Tag from "../../components/Tag.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
memberships: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: (data) => data.person.memberships.elements,
|
||||
skip() {
|
||||
return !this.currentActor || !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
post: {
|
||||
query: FETCH_POST,
|
||||
variables() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.slug;
|
||||
},
|
||||
error({ graphQLErrors }) {
|
||||
this.handleErrors(graphQLErrors);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Tag,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
title: this.post ? this.post.title : "",
|
||||
// all titles will be injected into this template
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
titleTemplate: this.post ? "%s | Mobilizon" : "Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Post extends Vue {
|
||||
@Prop({ required: true, type: String }) slug!: string;
|
||||
|
||||
post!: IPost;
|
||||
|
||||
memberships!: IMember[];
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
get isCurrentActorMember(): boolean {
|
||||
if (!this.post.attributedTo || !this.memberships) return false;
|
||||
return this.memberships.map(({ parent: { id } }) => id).includes(this.post.attributedTo.id);
|
||||
}
|
||||
|
||||
async handleErrors(errors: GraphQLError[]) {
|
||||
if (errors[0].message.includes("No such post")) {
|
||||
await this.$router.push({ name: RouteName.PAGE_NOT_FOUND });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables.scss";
|
||||
|
||||
article {
|
||||
section.heading-section {
|
||||
text-align: center;
|
||||
|
||||
h1.title {
|
||||
margin: 0 auto;
|
||||
padding-top: 3rem;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.authors {
|
||||
margin-top: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.published {
|
||||
margin-top: 1rem;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: 0.4rem;
|
||||
margin-bottom: 2rem;
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: $purple-1;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
section.content {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
section.tags {
|
||||
padding-bottom: 5rem;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
span {
|
||||
&.tag {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: $white;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 0 3rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user