mirror of
https://github.com/aelve/guide.git
synced 2024-11-23 12:15:06 +03:00
feat/category item info edit (#250)
* added search results nothing found text * removed external spaces and lines * Item info update
This commit is contained in:
parent
c32852ab77
commit
845a7a671e
@ -10,7 +10,7 @@
|
||||
wrap
|
||||
>
|
||||
<span class="mr-3">
|
||||
made by
|
||||
made by
|
||||
<a-link
|
||||
openInNewTab
|
||||
url="https://aelve.com/"
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<a
|
||||
class="link"
|
||||
:target="openInNewTab ? '_blank': ''"
|
||||
:rel="openInNewTab ? 'noopener noreferrer': ''"
|
||||
:target="openInNewTab ? '_blank': ''"
|
||||
:rel="openInNewTab ? 'noopener noreferrer': ''"
|
||||
:href="url"
|
||||
>
|
||||
<slot />
|
||||
@ -17,5 +17,4 @@ export default class ALink extends Vue {
|
||||
@Prop(Boolean) openInNewTab!: boolean
|
||||
@Prop(String) url!: string
|
||||
}
|
||||
|
||||
</script>
|
@ -5,7 +5,7 @@
|
||||
max-width="500px"
|
||||
>
|
||||
<slot slot="activator" />
|
||||
|
||||
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-form
|
||||
@ -78,7 +78,6 @@ export default class AddItemDialog extends Vue {
|
||||
category: this.categoryId,
|
||||
name: this.itemName
|
||||
})
|
||||
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
@ -32,22 +32,21 @@
|
||||
<div v-html="categoryDescription" />
|
||||
</div>
|
||||
<template v-if="category">
|
||||
<div
|
||||
v-for="(value, index) in category.items"
|
||||
:key="index"
|
||||
>
|
||||
<article-content
|
||||
:kind="value.name"
|
||||
:group="value.group"
|
||||
:itemDescription="value.description.html"
|
||||
:pros="value.pros"
|
||||
:cons="value.cons"
|
||||
:ecosystem="value.ecosystem.html"
|
||||
:tocArray="value.toc"
|
||||
:notes="value.notes.html"
|
||||
:itemUid="value.uid"
|
||||
/>
|
||||
</div>
|
||||
<category-item
|
||||
v-for="value in category.items"
|
||||
:key="value.uid"
|
||||
:itemUid="value.uid"
|
||||
:link="value.link"
|
||||
:name="value.name"
|
||||
:group="value.group"
|
||||
:itemDescription="value.description.html"
|
||||
:pros="value.pros"
|
||||
:cons="value.cons"
|
||||
:ecosystem="value.ecosystem.html"
|
||||
:tocArray="value.toc"
|
||||
:notes="value.notes.html"
|
||||
:kind="value.kind"
|
||||
/>
|
||||
</template>
|
||||
<v-btn
|
||||
flat
|
||||
@ -58,7 +57,7 @@
|
||||
<v-icon class="mr-1" left>add</v-icon>
|
||||
Add new item
|
||||
</v-btn>
|
||||
<add-item-dialog
|
||||
<add-item-dialog
|
||||
v-model="isDialogOpen"
|
||||
:categoryId="categoryId"
|
||||
/>
|
||||
@ -70,14 +69,14 @@
|
||||
import _toKebabCase from 'lodash/kebabCase'
|
||||
import _get from 'lodash/get'
|
||||
import { Vue, Component, Prop } from 'vue-property-decorator'
|
||||
import ArticleContent from 'client/components/ArticleContent.vue'
|
||||
import CategoryItem from 'client/components/CategoryItem.vue'
|
||||
import AddItemDialog from 'client/components/AddItemDialog.vue'
|
||||
import category from 'client/store/modules/category'
|
||||
|
||||
@Component({
|
||||
name: 'article-component',
|
||||
components: {
|
||||
ArticleContent,
|
||||
CategoryItem,
|
||||
AddItemDialog
|
||||
}
|
||||
})
|
||||
|
@ -15,13 +15,13 @@
|
||||
xl1
|
||||
v-for="(groupCategories, groupName, index) in groups"
|
||||
:key="index"
|
||||
>
|
||||
>
|
||||
<div class="category-group">
|
||||
<h4 class="mb-2 display-1 font-weight-black category-group-name">
|
||||
{{ groupName }}
|
||||
</h4>
|
||||
|
||||
<router-link
|
||||
<router-link
|
||||
class="category-title"
|
||||
v-for="category in groupCategories[CategoryStatus.finished]"
|
||||
:key="category.uid"
|
||||
|
@ -1,36 +1,28 @@
|
||||
<template>
|
||||
<div class="article-item">
|
||||
<div class="article-header">
|
||||
<p class="article-hd-textlg">{{ kind }}</p>
|
||||
<a-link
|
||||
openInNewTab :url="`http://hackage.haskell.org/package/${kind}`"
|
||||
class="article-header-link">
|
||||
(Hackage)
|
||||
</a-link>
|
||||
<p class="article-hd-textsm">{{ group }}</p>
|
||||
<div class="article-header-icons">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
<div class="header-func-icons">
|
||||
<i class="fas fa-cogs"></i>
|
||||
<button @click="openConfirmDialog">
|
||||
<i class="fas fa-times item-del-btn"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-content">
|
||||
|
||||
<category-item-toolbar
|
||||
:itemUid="itemUid"
|
||||
:itemName="name"
|
||||
:itemLink="link"
|
||||
:itemGroup="group"
|
||||
:itemKind="kind"
|
||||
/>
|
||||
|
||||
<div class="category-item-body">
|
||||
<p class="article-section-title">Summary</p>
|
||||
<div
|
||||
class="article-description"
|
||||
v-html="itemDescription"/>
|
||||
<div
|
||||
class="article-description"
|
||||
v-html="itemDescription"
|
||||
/>
|
||||
<div class="flex-wrapper article-section pros-cons-box">
|
||||
<div class="width-50">
|
||||
<p class="article-section-title">Pros</p>
|
||||
<ul
|
||||
v-if="pros"
|
||||
v-for="(value, index) in pros"
|
||||
:key="index">
|
||||
<ul
|
||||
v-if="pros"
|
||||
v-for="(value, index) in pros"
|
||||
:key="index"
|
||||
>
|
||||
<li v-html="value.content.html"/>
|
||||
</ul>
|
||||
</div>
|
||||
@ -53,53 +45,51 @@
|
||||
<button class="notes-settings-btn" @click="collapseNotes">collapse notes</button>
|
||||
<button class="notes-settings-btn" style="display: none;">edit notes</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(value, index) in tocArray"
|
||||
:key="index">
|
||||
<!-- TODO refactor v-for from ul to li -->
|
||||
<ul
|
||||
v-for="(value, index) in value"
|
||||
:key="index">
|
||||
<li
|
||||
class="notes-toc-item"
|
||||
v-if="value.content">
|
||||
<a
|
||||
:href="`#${value.slug}`"
|
||||
@click="expandNotes">
|
||||
<div
|
||||
v-for="(value, index) in tocArray"
|
||||
:key="index"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
class="notes-toc-item"
|
||||
:key="index"
|
||||
v-for="(value, index) in value"
|
||||
v-if="value.content"
|
||||
>
|
||||
<a
|
||||
:href="`#${value.slug}`"
|
||||
@click="expandNotes"
|
||||
>
|
||||
<p>{{value.content.html}}</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- TODO lookslike transition not working -->
|
||||
<transition name="slidedown">
|
||||
<div
|
||||
class="notes-content"
|
||||
v-show="isNoteExpanded"
|
||||
v-html="notes">
|
||||
</div>
|
||||
v-show="isNoteExpanded"
|
||||
v-html="notes"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<confirm-dialog
|
||||
confirmationText="delete this item"
|
||||
v-model="isDeleteItemDialogOpen"
|
||||
@confirmed="deleteItem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from 'vue-property-decorator'
|
||||
import ConfirmDialog from 'client/components/ConfirmDialog.vue'
|
||||
import CategoryItemToolbar from 'client/components/CategoryItemToolbar.vue'
|
||||
import { ICategoryItem } from 'client/service/CategoryItem.ts'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ConfirmDialog
|
||||
CategoryItemToolbar
|
||||
}
|
||||
})
|
||||
export default class ArticleContent extends Vue {
|
||||
@Prop(String) kind!: string
|
||||
export default class CategoryItem extends Vue {
|
||||
@Prop(String) name!: string
|
||||
@Prop(String) group!: string
|
||||
@Prop(String) itemDescription!: string
|
||||
@Prop(Array) pros!: [any]
|
||||
@ -109,9 +99,10 @@ export default class ArticleContent extends Vue {
|
||||
@Prop(Object) tocItemContent!: object
|
||||
@Prop(String) notes!: string
|
||||
@Prop(String) itemUid!: string
|
||||
@Prop(String) link!: string
|
||||
@Prop(Object) kind!: object
|
||||
|
||||
isNoteExpanded: boolean = false
|
||||
isDeleteItemDialogOpen: boolean = false
|
||||
|
||||
expandNotes () {
|
||||
this.isNoteExpanded = true
|
||||
@ -120,24 +111,20 @@ export default class ArticleContent extends Vue {
|
||||
collapseNotes () {
|
||||
this.isNoteExpanded = false
|
||||
}
|
||||
|
||||
async deleteItem (): Promise<void> {
|
||||
await this.$store.dispatch('categoryItem/deleteItemById', this.itemUid)
|
||||
}
|
||||
|
||||
openConfirmDialog () {
|
||||
this.isDeleteItemDialogOpen = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-content >>> p {
|
||||
.category-item-body {
|
||||
padding: 15px 20px 25px;
|
||||
}
|
||||
|
||||
.category-item-body >>> p {
|
||||
font-size: 16px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.article-content >>> li {
|
||||
.category-item-body >>> li {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@ -177,18 +164,9 @@ export default class ArticleContent extends Vue {
|
||||
|
||||
.article-item {
|
||||
background: #e5e5e5;
|
||||
padding: 15px 20px 25px;
|
||||
margin: 0 0 80px;
|
||||
}
|
||||
|
||||
.article-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
margin: -15px -20px 15px;
|
||||
background: #c8c8c8;
|
||||
}
|
||||
|
||||
.flex-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
@ -210,46 +188,6 @@ export default class ArticleContent extends Vue {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.article-hd-textlg {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.article-hd-textsm {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.article-header-link {
|
||||
font-size: 22px;
|
||||
padding: 0 32px 0 8px;
|
||||
}
|
||||
|
||||
.article-header-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.article-header-icons >>> i {
|
||||
margin-right: 5px;
|
||||
font-size: 18px;
|
||||
color: #979797;
|
||||
cursor: pointer;
|
||||
transition: all ease-in-out 0.25s;
|
||||
}
|
||||
|
||||
.article-header-icons >>> i:nth-last-child(1) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.article-header-icons >>> i:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.header-func-icons {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.notes-toc-item >>> p {
|
||||
margin: 0;
|
||||
}
|
||||
@ -298,21 +236,11 @@ export default class ArticleContent extends Vue {
|
||||
}
|
||||
|
||||
@media screend and (max-width: 768px) {
|
||||
.article-content {
|
||||
.category-item-body {
|
||||
width: 100%;
|
||||
}
|
||||
.article-item {
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
.article-hd-textlg {
|
||||
font-size: 20px;
|
||||
}
|
||||
.article-hd-textsm {
|
||||
font-size: 16px;
|
||||
}
|
||||
.article-header-link {
|
||||
font-size: 20px;
|
||||
padding: 0 32px 0 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
195
front/client/components/CategoryItemToolbar.vue
Normal file
195
front/client/components/CategoryItemToolbar.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<v-expansion-panel class="category-item-toolbar-wrapper">
|
||||
<v-expansion-panel-content
|
||||
hide-actions
|
||||
lazy
|
||||
:value="isEditItemInfoMenuOpen"
|
||||
>
|
||||
<v-toolbar
|
||||
slot="header"
|
||||
color="#d6d6d6"
|
||||
class="category-item-toolbar"
|
||||
@click.stop=""
|
||||
>
|
||||
<v-toolbar-title class="headline">
|
||||
<a-link
|
||||
v-if="itemLink"
|
||||
:url="itemLink"
|
||||
openInNewTab
|
||||
>
|
||||
{{ itemName }}
|
||||
</a-link>
|
||||
<span v-else> {{ itemName }} </span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items class="category-item-toolbar-btns-wrapper">
|
||||
<v-btn
|
||||
flat
|
||||
icon
|
||||
title="move item down"
|
||||
>
|
||||
<v-icon>fas fa-arrow-up</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
flat
|
||||
icon
|
||||
title="move item down"
|
||||
>
|
||||
<v-icon>fas fa-arrow-down</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
flat
|
||||
icon
|
||||
title="edit item info"
|
||||
@click="toggleEditItemInfoMenu"
|
||||
>
|
||||
<v-icon>fas fa-cog</v-icon>
|
||||
<v-icon
|
||||
v-if="isItemInfoEdited"
|
||||
class="edit-item-info-changed-icon"
|
||||
color="#6495ed"
|
||||
size="8"
|
||||
>
|
||||
fas fa-circle
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
flat
|
||||
icon
|
||||
title="delete item"
|
||||
@click="openConfirmDeleteDialog"
|
||||
>
|
||||
<v-icon>fas fa-times</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-layout column class="pa-3">
|
||||
<v-flex>
|
||||
<v-text-field
|
||||
v-model="itemNameEdit"
|
||||
label="Name"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="itemLinkEdit"
|
||||
label="Site (optional)"
|
||||
/>
|
||||
</v-flex>
|
||||
<v-flex align-self-end>
|
||||
<v-btn
|
||||
class="mr-0"
|
||||
:disabled="!isItemInfoEdited"
|
||||
@click="saveItemInfo"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-expansion-panel-content>
|
||||
|
||||
<confirm-dialog
|
||||
confirmationText="delete this item"
|
||||
v-model="isDeleteItemDialogOpen"
|
||||
@confirmed="deleteItem"
|
||||
/>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'
|
||||
import ConfirmDialog from 'client/components/ConfirmDialog.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ConfirmDialog
|
||||
}
|
||||
})
|
||||
export default class CategoryItemToolbar extends Vue {
|
||||
@Prop(String) itemUid!: string
|
||||
@Prop(String) itemName!: string
|
||||
@Prop(String) itemLink!: string
|
||||
@Prop(String) itemGroup!: string
|
||||
@Prop(Object) itemKind!: object
|
||||
|
||||
isEditItemInfoMenuOpen: boolean = false
|
||||
isDeleteItemDialogOpen: boolean = false
|
||||
itemNameEdit: string = this.itemName
|
||||
itemLinkEdit: string = this.itemLink
|
||||
|
||||
@Watch('itemName')
|
||||
onItemNameChange (newVal: string) {
|
||||
this.itemNameEdit = newVal
|
||||
}
|
||||
|
||||
@Watch('itemLink')
|
||||
onItemLinkChange (newVal: string) {
|
||||
this.itemLinkEdit = newVal
|
||||
}
|
||||
|
||||
toggleEditItemInfoMenu () {
|
||||
this.isEditItemInfoMenuOpen = !this.isEditItemInfoMenuOpen
|
||||
}
|
||||
|
||||
openConfirmDeleteDialog () {
|
||||
this.isDeleteItemDialogOpen = true
|
||||
}
|
||||
|
||||
get isItemInfoEdited () {
|
||||
return this.itemName !== this.itemNameEdit || this.itemLink !== this.itemLinkEdit
|
||||
}
|
||||
|
||||
async saveItemInfo (): Promise<void> {
|
||||
await this.$store.dispatch('categoryItem/updateItemInfo', {
|
||||
id: this.itemUid,
|
||||
body: {
|
||||
name: this.itemNameEdit,
|
||||
link: this.itemLinkEdit,
|
||||
// TODO remove next two lines after changing API of editing item info
|
||||
// https://www.notion.so/aelve/Tasks-a157d6e3f22241ae83cf624fec3aaad5?p=fe081421dcf844e79e8877d9f4a103ad
|
||||
created: '2016-07-22T00:00:00Z',
|
||||
uid: this.itemUid,
|
||||
group: this.itemGroup,
|
||||
kind: this.itemKind
|
||||
}
|
||||
})
|
||||
await this.$store.dispatch('category/reloadCategory')
|
||||
this.toggleEditItemInfoMenu()
|
||||
}
|
||||
|
||||
async deleteItem (): Promise<void> {
|
||||
await this.$store.dispatch('categoryItem/deleteItemById', this.itemUid)
|
||||
await this.$store.dispatch('category/reloadCategory')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.edit-item-info-changed-icon {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
.category-item-toolbar >>> .v-toolbar__title {
|
||||
overflow: visible;
|
||||
}
|
||||
.category-item-toolbar-wrapper {
|
||||
display: flex;
|
||||
box-shadow: none;
|
||||
/* background: #c8c8c8; */
|
||||
}
|
||||
.category-item-toolbar-wrapper >>> .v-expansion-panel__header {
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
cursor: unset;
|
||||
/* background: #d6d6d6; */
|
||||
}
|
||||
.category-item-toolbar-wrapper >>> .v-expansion-panel__body {
|
||||
background: #d6d6d6;
|
||||
}
|
||||
.category-item-toolbar-btns-wrapper > * {
|
||||
margin: 0 2px;
|
||||
color: #979797;
|
||||
}
|
||||
</style>
|
@ -14,7 +14,7 @@
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
<v-btn
|
||||
flat
|
||||
color="primary"
|
||||
class="confirm-btn"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<article-item
|
||||
<article-item
|
||||
:categoryId="categoryId"
|
||||
/>
|
||||
</template>
|
||||
|
@ -41,7 +41,7 @@
|
||||
:url="`http://aelve.com:4801/haskell/${result.contents.category.uid}#item-${result.contents.info.uid}`"
|
||||
>
|
||||
<!-- Do not format next line to separate lines cause it adds extra space after </a-link>. -->
|
||||
{{ result.contents.info.name }}</a-link>'s ecosystem
|
||||
{{ result.contents.info.name }}</a-link>'s ecosystem
|
||||
</span>
|
||||
</span>
|
||||
</v-card-title>
|
||||
|
@ -2,6 +2,7 @@ import axios from 'axios'
|
||||
import { ICategoryFull } from './Category'
|
||||
|
||||
class CategoryItemService {
|
||||
// TODO replace all axios api request to axios instance to remove duplication of 'api/*'
|
||||
async createItem ({ category, name }: ICreateCategoryItem) {
|
||||
const { data } = await axios.post(`api/item/${category}`, null, {
|
||||
params: {
|
||||
@ -13,6 +14,9 @@ class CategoryItemService {
|
||||
async deleteItemById (id: ICategoryItem['uid']): Promise<void> {
|
||||
await axios.delete(`api/item/${id}`)
|
||||
}
|
||||
async updateItemInfo (id: ICategoryItem['uid'], body: ICategoryItemInfo): Promise<void> {
|
||||
await axios.put(`api/item/${id}/info`, body)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICreateCategoryItem {
|
||||
@ -35,6 +39,15 @@ export interface ICategoryItem {
|
||||
|
||||
}
|
||||
|
||||
export interface ICategoryItemInfo {
|
||||
|
||||
uid?: string
|
||||
name?: string
|
||||
created?: string
|
||||
link?: string
|
||||
kind?: object
|
||||
}
|
||||
|
||||
export interface ITrait {
|
||||
uid: string
|
||||
content: object
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { ActionTree, GetterTree, MutationTree, ActionContext, Module } from 'vuex'
|
||||
import { ICategoryItem, CategoryItemService, ICreateCategoryItem } from 'client/service/CategoryItem'
|
||||
import { CategoryService, ICategoryFull } from 'client/service/Category'
|
||||
import {
|
||||
ICategoryItem,
|
||||
CategoryItemService,
|
||||
ICreateCategoryItem,
|
||||
ICategoryItemInfo
|
||||
} from 'client/service/CategoryItem'
|
||||
|
||||
interface ICategoryItemState {
|
||||
categoryItemList: ICategoryItem[]
|
||||
@ -24,9 +28,14 @@ const actions: ActionTree<ICategoryItemState, any> = {
|
||||
dispatch('category/reloadCategory', null, { root: true })
|
||||
return createdId
|
||||
},
|
||||
async deleteItemById ({ dispatch }: ActionContext<ICategoryItemState, any>, id: ICategoryItem['uid']) {
|
||||
async deleteItemById (context, id: ICategoryItem['uid']) {
|
||||
await CategoryItemService.deleteItemById(id)
|
||||
dispatch('category/reloadCategory', null, { root: true })
|
||||
},
|
||||
async updateItemInfo (
|
||||
context: ActionContext<ICategoryItemState, any>,
|
||||
{ id, body }: { id: ICategoryItem['uid'], body: ICategoryItemInfo }
|
||||
): Promise<void> {
|
||||
await CategoryItemService.updateItemInfo(id, body)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user