Merge pull request #969 from Human-Connection/967-filter-post-by-category

Filter posts by category
This commit is contained in:
Robert Schäfer 2019-07-16 00:50:56 +02:00 committed by GitHub
commit b97f7e464f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 659 additions and 125 deletions

View File

@ -18,10 +18,11 @@ const createPostWithCategoriesMutation = `
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
id
title
}
}
`
const creatPostWithCategoriesVariables = {
const createPostWithCategoriesVariables = {
title: postTitle,
content: postContent,
categoryIds: ['cat9', 'cat4', 'cat15'],
@ -35,6 +36,26 @@ const postQueryWithCategories = `
}
}
`
const createPostWithoutCategoriesVariables = {
title: 'This is a post without categories',
content: 'I should be able to filter it out',
categoryIds: null,
}
const postQueryFilteredByCategory = `
query Post($filter: _PostFilter) {
Post(filter: $filter) {
title
id
categories {
id
}
}
}
`
const postCategoriesFilterParam = { categories_some: { id_in: ['cat4'] } }
const postQueryFilteredByCategoryVariables = {
filter: postCategoriesFilterParam,
}
beforeEach(async () => {
userParams = {
name: 'TestUser',
@ -133,7 +154,8 @@ describe('CreatePost', () => {
})
describe('categories', () => {
it('allows a user to set the categories of the post', async () => {
let postWithCategories
beforeEach(async () => {
await Promise.all([
factory.create('Category', {
id: 'cat9',
@ -151,18 +173,39 @@ describe('CreatePost', () => {
icon: 'shopping-cart',
}),
])
const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
const postWithCategories = await client.request(
postWithCategories = await client.request(
createPostWithCategoriesMutation,
creatPostWithCategoriesVariables,
createPostWithCategoriesVariables,
)
})
it('allows a user to set the categories of the post', async () => {
const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
const postQueryWithCategoriesVariables = {
id: postWithCategories.CreatePost.id,
}
await expect(
client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
})
it('allows a user to filter for posts by category', async () => {
await client.request(createPostWithCategoriesMutation, createPostWithoutCategoriesVariables)
const categoryIds = [{ id: 'cat4' }, { id: 'cat15' }, { id: 'cat9' }]
const expected = {
Post: [
{
title: postTitle,
id: postWithCategories.CreatePost.id,
categories: expect.arrayContaining(categoryIds),
},
],
}
await expect(
client.request(postQueryFilteredByCategory, postQueryFilteredByCategoryVariables),
).resolves.toEqual(expected)
})
})
})
})
@ -260,7 +303,7 @@ describe('UpdatePost', () => {
])
postWithCategories = await client.request(
createPostWithCategoriesMutation,
creatPostWithCategoriesVariables,
createPostWithCategoriesVariables,
)
updatePostVariables = {
id: postWithCategories.CreatePost.id,

View File

@ -22,16 +22,16 @@ Feature: Tags and Categories
When I navigate to the administration dashboard
And I click on the menu item "Categories"
Then I can see the following table:
| | Name | Posts |
| | Just For Fun | 2 |
| | Happyness & Values | 1 |
| | Health & Wellbeing | 0 |
| | Name | Posts |
| | Just For Fun | 2 |
| | Happyness & Values | 1 |
| | Health & Wellbeing | 0 |
Scenario: See an overview of tags
When I navigate to the administration dashboard
And I click on the menu item "Tags"
Then I can see the following table:
| | Name | Users | Posts |
| 1 | Democracy | 3 | 4 |
| 2 | Nature | 2 | 3 |
| 3 | Ecology | 1 | 1 |
| | Name | Users | Posts |
| 1 | Democracy | 3 | 4 |
| 2 | Nature | 2 | 3 |
| 3 | Ecology | 1 | 1 |

View File

@ -1,36 +1,36 @@
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
import { When, Then } from "cypress-cucumber-preprocessor/steps";
/* global cy */
When('I visit my profile page', () => {
cy.openPage('profile/peter-pan')
})
When("I visit my profile page", () => {
cy.openPage("profile/peter-pan");
});
Then('I should be able to change my profile picture', () => {
const avatarUpload = 'onourjourney.png'
Then("I should be able to change my profile picture", () => {
const avatarUpload = "onourjourney.png";
cy.fixture(avatarUpload, 'base64').then(fileContent => {
cy.get('#customdropzone').upload(
{ fileContent, fileName: avatarUpload, mimeType: 'image/png' },
{ subjectType: 'drag-n-drop' }
)
})
cy.get('.profile-avatar img')
.should('have.attr', 'src')
.and('contains', 'onourjourney')
cy.contains('.iziToast-message', 'Upload successful').should(
'have.length',
cy.fixture(avatarUpload, "base64").then(fileContent => {
cy.get("#customdropzone").upload(
{ fileContent, fileName: avatarUpload, mimeType: "image/png" },
{ subjectType: "drag-n-drop", force: true }
);
});
cy.get(".profile-avatar img")
.should("have.attr", "src")
.and("contains", "onourjourney");
cy.contains(".iziToast-message", "Upload successful").should(
"have.length",
1
)
})
);
});
When("I visit another user's profile page", () => {
cy.openPage('profile/peter-pan')
})
cy.openPage("profile/peter-pan");
});
Then('I cannot upload a picture', () => {
cy.get('.ds-card-content')
Then("I cannot upload a picture", () => {
cy.get(".ds-card-content")
.children()
.should('not.have.id', 'customdropzone')
.should('have.class', 'ds-avatar')
})
.should("not.have.id", "customdropzone")
.should("have.class", "ds-avatar");
});

View File

@ -27,7 +27,7 @@
</template>
<script>
import gql from 'graphql-tag'
import CategoryQuery from '~/graphql/CategoryQuery.js'
export default {
props: {
@ -85,13 +85,7 @@ export default {
apollo: {
Category: {
query() {
return gql(`{
Category {
id
name
icon
}
}`)
return CategoryQuery()
},
result(result) {
this.categories = result.data.Category

View File

@ -0,0 +1,134 @@
import { mount, createLocalVue } from '@vue/test-utils'
import VTooltip from 'v-tooltip'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import FilterPosts from './FilterPosts.vue'
import FilterPostsMenuItem from './FilterPostsMenuItems.vue'
import { mutations } from '~/store/posts'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VTooltip)
localVue.use(Vuex)
describe('FilterPosts.vue', () => {
let wrapper
let mocks
let propsData
let menuToggle
let allCategoriesButton
let environmentAndNatureButton
let consumptionAndSustainabiltyButton
let democracyAndPoliticsButton
beforeEach(() => {
mocks = {
$apollo: {
query: jest
.fn()
.mockResolvedValueOnce({
data: { Post: { title: 'Post with Category', category: [{ id: 'cat4' }] } },
})
.mockRejectedValue({ message: 'We were unable to filter' }),
},
$t: jest.fn(),
$i18n: {
locale: () => 'en',
},
$toast: {
error: jest.fn(),
},
}
propsData = {
categories: [
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' },
],
}
})
describe('mount', () => {
const store = new Vuex.Store({
mutations: {
'posts/SET_POSTS': mutations.SET_POSTS,
},
})
const Wrapper = () => {
return mount(FilterPosts, { mocks, localVue, propsData, store })
}
beforeEach(() => {
wrapper = Wrapper()
menuToggle = wrapper.findAll('a').at(0)
menuToggle.trigger('click')
})
it('groups the categories by pair', () => {
expect(wrapper.vm.chunk).toEqual([
[
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
],
[{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' }],
])
})
it('starts with all categories button active', () => {
allCategoriesButton = wrapper.findAll('button').at(0)
expect(allCategoriesButton.attributes().class).toContain('ds-button-primary')
})
it('adds a categories id to selectedCategoryIds when clicked', () => {
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual(['cat4'])
})
it('sets primary to true when the button is clicked', () => {
democracyAndPoliticsButton = wrapper.findAll('button').at(3)
democracyAndPoliticsButton.trigger('click')
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary')
})
it('queries a post by its categories', () => {
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
consumptionAndSustainabiltyButton.trigger('click')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
filter: { categories_some: { id_in: ['cat15'] } },
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
})
it('supports a query of multiple categories', () => {
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
consumptionAndSustainabiltyButton.trigger('click')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
filter: { categories_some: { id_in: ['cat4', 'cat15'] } },
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
})
it('toggles the categoryIds when clicked more than once', () => {
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
environmentAndNatureButton.trigger('click')
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual([])
})
})
})

View File

@ -0,0 +1,61 @@
<template>
<dropdown ref="menu" :placement="placement" :offset="offset">
<a slot="default" slot-scope="{ toggleMenu }" href="#" @click.prevent="toggleMenu()">
<ds-icon style="margin: 12px 0px 0px 10px;" name="filter" size="large" />
<ds-icon style="margin: 7px 0px 0px 2px" size="xx-small" name="angle-down" />
</a>
<template slot="popover">
<filter-posts-menu-items :chunk="chunk" @filterPosts="filterPosts" />
</template>
</dropdown>
</template>
<script>
import _ from 'lodash'
import Dropdown from '~/components/Dropdown'
import { filterPosts } from '~/graphql/PostQuery.js'
import { mapMutations } from 'vuex'
import FilterPostsMenuItems from '~/components/FilterPosts/FilterPostsMenuItems'
export default {
components: {
Dropdown,
FilterPostsMenuItems,
},
props: {
placement: { type: String },
offset: { type: [String, Number] },
categories: { type: Array, default: () => [] },
},
data() {
return {
pageSize: 12,
}
},
computed: {
chunk() {
return _.chunk(this.categories, 2)
},
},
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
filterPosts(categoryIds) {
const filter = categoryIds.length ? { categories_some: { id_in: categoryIds } } : {}
this.$apollo
.query({
query: filterPosts(this.$i18n),
variables: {
filter: filter,
first: this.pageSize,
offset: 0,
},
})
.then(({ data: { Post } }) => {
this.setPosts(Post)
})
.catch(error => this.$toast.error(error.message))
},
},
}
</script>

View File

@ -0,0 +1,126 @@
<template>
<ds-container>
<ds-space />
<ds-flex id="filter-posts-header">
<ds-heading tag="h4">{{ $t('filter-posts.header') }}</ds-heading>
<ds-space margin-bottom="large" />
</ds-flex>
<ds-flex>
<ds-flex-item
:width="{ base: '100%', sm: '100%', md: '100%', lg: '5%' }"
class="categories-menu-item"
>
<ds-flex>
<ds-flex-item width="10%" />
<ds-flex-item width="100%">
<ds-button
icon="check"
@click.stop.prevent="toggleCategory()"
:primary="allCategories"
/>
<ds-flex-item>
<label class="category-labels">{{ $t('filter-posts.all') }}</label>
</ds-flex-item>
<ds-space />
</ds-flex-item>
</ds-flex>
</ds-flex-item>
<ds-flex-item :width="{ base: '0%', sm: '0%', md: '0%', lg: '4%' }" />
<ds-flex-item
:width="{ base: '0%', sm: '0%', md: '0%', lg: '3%' }"
id="categories-menu-divider"
/>
<ds-flex-item
:width="{ base: '50%', sm: '50%', md: '50%', lg: '11%' }"
v-for="index in chunk.length"
:key="index"
>
<ds-flex v-for="category in chunk[index - 1]" :key="category.id" class="categories-menu">
<ds-flex class="categories-menu">
<ds-flex-item width="100%" class="categories-menu-item">
<ds-button
:icon="category.icon"
:primary="isActive(category.id)"
@click.stop.prevent="toggleCategory(category.id)"
/>
<ds-space margin-bottom="small" />
</ds-flex-item>
<ds-flex>
<ds-flex-item class="categories-menu-item">
<label class="category-labels">{{ category.name }}</label>
</ds-flex-item>
<ds-space margin-bottom="xx-large" />
</ds-flex>
</ds-flex>
</ds-flex>
</ds-flex-item>
</ds-flex>
</ds-container>
</template>
<script>
export default {
props: {
chunk: { type: Array, default: () => [] },
},
data() {
return {
selectedCategoryIds: [],
allCategories: true,
}
},
methods: {
isActive(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
return true
}
return false
},
toggleCategory(id) {
if (!id) {
this.selectedCategoryIds = []
this.allCategories = true
} else {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
this.selectedCategoryIds.splice(index, 1)
} else {
this.selectedCategoryIds.push(id)
}
this.allCategories = false
}
this.$emit('filterPosts', this.selectedCategoryIds)
},
},
}
</script>
<style lang="scss">
#filter-posts-header {
display: block;
}
.categories-menu-item {
text-align: center;
}
.categories-menu {
justify-content: center;
}
.category-labels {
font-size: $font-size-small;
}
@media only screen and (min-width: 960px) {
#categories-menu-divider {
border-left: 1px solid $border-color-soft;
margin: 9px 0px 40px 0px;
}
}
@media only screen and (max-width: 960px) {
#filter-posts-header {
text-align: center;
}
}
</style>

View File

@ -2,7 +2,7 @@
<ds-button v-if="totalNotifications <= 0" class="notifications-menu" disabled icon="bell">
{{ totalNotifications }}
</ds-button>
<dropdown v-else class="notifications-menu">
<dropdown v-else class="notifications-menu" :placement="placement">
<template slot="default" slot-scope="{ toggleMenu }">
<ds-button primary icon="bell" @click.prevent="toggleMenu">
{{ totalNotifications }}
@ -48,6 +48,9 @@ export default {
NotificationList,
Dropdown,
},
props: {
placement: { type: String },
},
computed: {
totalNotifications() {
return (this.notifications || []).length

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export default () => {
return gql(`{
Category {
id
name
icon
}
}`)
}

View File

@ -75,3 +75,48 @@ export default i18n => {
}
`)
}
export const filterPosts = i18n => {
const lang = i18n.locale().toUpperCase()
return gql(`
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
Post(filter: $filter, first: $first, offset: $offset) {
id
title
contentExcerpt
createdAt
disabled
deleted
slug
image
author {
id
avatar
slug
name
disabled
deleted
contributionsCount
shoutedCount
commentsCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
icon
}
}
commentsCount
categories {
id
name
icon
}
shoutedCount
}
}
`)
}

View File

@ -3,14 +3,24 @@
<div class="main-navigation">
<ds-container class="main-navigation-container" style="padding: 10px 10px;">
<div>
<ds-flex>
<ds-flex-item :width="{ base: '49px', md: '150px' }">
<nuxt-link to="/">
<ds-flex class="main-navigation-flex">
<ds-flex-item :width="{ lg: '3.5%' }" />
<ds-flex-item :width="{ base: '80%', sm: '80%', md: '80%', lg: '15%' }">
<a @click="redirectToRoot">
<ds-logo />
</nuxt-link>
</a>
</ds-flex-item>
<ds-flex-item>
<div id="nav-search-box" v-on:click="unfolded" @blur.capture="foldedup">
<ds-flex-item
:width="{ base: '20%', sm: '20%', md: '20%', lg: '0%' }"
class="mobile-hamburger-menu"
>
<ds-button icon="bars" @click="toggleMobileMenuView" right />
</ds-flex-item>
<ds-flex-item
:width="{ base: '85%', sm: '85%', md: '50%', lg: '50%' }"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
>
<div id="nav-search-box">
<search-input
id="nav-search"
:delay="300"
@ -22,17 +32,36 @@
/>
</div>
</ds-flex-item>
<ds-flex-item width="200px" style="background-color:white">
<div class="main-navigation-right" style="float:right">
<ds-flex-item
:width="{ base: '15%', sm: '15%', md: '10%', lg: '10%' }"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
>
<no-ssr>
<filter-posts placement="top-start" offset="8" :categories="categories" />
</no-ssr>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '10%', lg: '2%' }" />
<ds-flex-item
:width="{ base: '100%', sm: '100%', md: '100%', lg: '13%' }"
style="background-color:white"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
>
<div
class="main-navigation-right"
:class="{
'desktop-view': !toggleMobileMenu,
'hide-mobile-menu': !toggleMobileMenu,
}"
>
<no-ssr>
<locale-switch class="topbar-locale-switch" placement="bottom" offset="23" />
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
</no-ssr>
<template v-if="isLoggedIn">
<no-ssr>
<notification-menu />
<notification-menu placement="top" />
</no-ssr>
<no-ssr>
<dropdown class="avatar-menu">
<dropdown class="avatar-menu" offset="8">
<template slot="default" slot-scope="{ toggleMenu }">
<a
class="avatar-menu-trigger"
@ -118,6 +147,8 @@ import NotificationMenu from '~/components/notifications/NotificationMenu'
import Dropdown from '~/components/Dropdown'
import HcAvatar from '~/components/Avatar/Avatar.vue'
import seo from '~/mixins/seo'
import FilterPosts from '~/components/FilterPosts/FilterPosts.vue'
import CategoryQuery from '~/graphql/CategoryQuery.js'
export default {
components: {
@ -127,11 +158,14 @@ export default {
Modal,
NotificationMenu,
HcAvatar,
FilterPosts,
},
mixins: [seo],
data() {
return {
mobileSearchVisible: false,
toggleMobileMenu: false,
categories: [],
}
},
computed: {
@ -180,10 +214,16 @@ export default {
return routes
},
},
watch: {
Category(category) {
this.categories = category || []
},
},
methods: {
...mapActions({
quickSearchClear: 'search/quickClear',
quickSearch: 'search/quickSearch',
fetchPosts: 'posts/fetchPosts',
}),
goToPost(item) {
this.$nextTick(() => {
@ -200,23 +240,24 @@ export default {
}
return this.$route.path.indexOf(url) === 0
},
unfolded: function() {
document.getElementById('nav-search-box').classList.add('unfolded')
toggleMobileMenuView() {
this.toggleMobileMenu = !this.toggleMobileMenu
},
foldedup: function() {
document.getElementById('nav-search-box').classList.remove('unfolded')
redirectToRoot() {
this.$router.replace('/')
this.fetchPosts({ i18n: this.$i18n, filter: {} })
},
},
apollo: {
Category: {
query() {
return CategoryQuery()
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
<style>
.unfolded {
position: absolute;
right: 0px;
left: 0px;
z-index: 1;
}
</style>
<style lang="scss">
.topbar-locale-switch {
@ -228,7 +269,7 @@ export default {
.main-container {
padding-top: 6rem;
padding-botton: 5rem;
padding-bottom: 5rem;
}
.main-navigation {
@ -242,6 +283,14 @@ export default {
flex: 1;
}
.main-navigation-right .desktop-view {
float: right;
}
.avatar-menu {
margin: 2px 0px 0px 5px;
}
.avatar-menu-trigger {
user-select: none;
display: flex;
@ -285,6 +334,24 @@ export default {
}
}
}
@media only screen and (min-width: 960px) {
.mobile-hamburger-menu {
display: none;
}
}
@media only screen and (max-width: 960px) {
#nav-search-box,
.main-navigation-right {
margin: 10px 0px;
}
.hide-mobile-menu {
display: none;
}
}
.ds-footer {
text-align: center;
position: fixed;

View File

@ -4,6 +4,10 @@
"hashtag-search": "Suche nach #{hashtag}",
"clearSearch": "Suche löschen"
},
"filter-posts": {
"header": "Themenkategorien",
"all": "Alle"
},
"site": {
"made": "Mit &#10084; gemacht",
"imprint": "Impressum",
@ -49,8 +53,8 @@
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
},
"submit": "Konto erstellen",
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt"
"submit": "Konto erstellen",
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt"
}
},
"create-user-account": {
@ -396,5 +400,4 @@
"terms": {
"text": "<div ><ol><li><strong>UNFALLGEFAHR: </strong>Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen.</li><br><li><strong>DU UND DEINE DATEN: </strong>Bitte beachte, dass wir die Inhalte der Alphaversion zu Werbezwecken, Webpräsentationen usw. verwenden, aber wir glauben, dass das auch in Deinem Interesse ist. Am besten keinen Nachnamen eingeben und bei noch mehr Datensparsamkeit ein Profilfoto ohne Identität verwenden. Mehr in unserer <a href='/pages/privacy' target='_blank'>Datenschutzerklärung</a>.</li><br><li><strong>BAUSTELLEN: </strong>Das ist immer noch eine Testversion. Wenn etwas nicht funktioniert, blockiert, irritiert, falsch angezeigt, verbogen oder nicht anklickbar ist, bitten wir dies zu entschuldigen. Fehler, Käfer und Bugs bitte melden! <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org/support</a></li><br><li><strong>VERHALTENSCODEX</strong>: Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an: <a href='https://alpha.human-connection.org/pages/code-of-conduct' target='_blank'>https://alpha.human-connection.org/pages/code-of-conduct</a></li><br><li><strong>MODERATION: </strong>Solange kein Community-Moderationssystem lauffähig ist, entscheidet ein Regenbogen-Einhorn darüber, ob Du körperlich und psychisch stabil genug bist, unsere Testversion zu bedienen. Das Einhorn kann Dich jederzeit von der Alpha entfernen. Also sei nett und lass Regenbogenfutter da!</li><br><li><strong>FAIRNESS: </strong>Sollte Dir die Alphaversion unseres Netzwerks wider Erwarten, egal aus welchen Gründen, nicht gefallen, überweisen wir Dir Deine gespendeten Monatsbeiträge innerhalb der ersten 2 Monate gerne zurück. Einfach Mail an: <a href='mailto:info@human-connection.org' target='_blank'>info@human-connection.org </a><strong>Achtung: Viele Funktionen werden erst nach und nach eingebaut. </strong></li><br><li><strong>FRAGEN?</strong> Die Termine und Links zu den Zoom-Räumen findest Du hier: <a href='http://localhost:3000/%22https://human-connection.org/events-und-news//%22' target='_blank'>https://human-connection.org/veranstaltungen/</a></li><br><li><strong>VON MENSCHEN FÜR MENSCHEN: </strong>Bitte hilf uns weitere monatlichen Spender für Human Connection zu bekommen, damit das Netzwerk so schnell wie möglich offiziell an den Start gehen kann. <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org</a></li></ol><p>Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎</p><br><p><strong>Herzlichst,</strong></p><p><strong>Euer Human Connection Team</strong></p></div>"
}
}

View File

@ -4,6 +4,10 @@
"hashtag-search": "Searching for #{hashtag}",
"clearSearch": "Clear search"
},
"filter-posts": {
"header": "Categories of Content",
"all": "All"
},
"site": {
"made": "Made with &#10084;",
"imprint": "Imprint",
@ -50,8 +54,8 @@
"email-exists": "There is already a user account with this email address!",
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
},
"submit": "Create an account",
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>"
"submit": "Create an account",
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>"
}
},
"create-user-account": {
@ -245,7 +249,6 @@
"more": "show more",
"less": "show less"
}
},
"quotes": {
"african": {

View File

@ -10,7 +10,7 @@
/>
</ds-flex-item>
<hc-post-card
v-for="(post, index) in uniq(Post)"
v-for="(post, index) in posts"
:key="post.id"
:post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
@ -33,11 +33,11 @@
<script>
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import gql from 'graphql-tag'
import uniqBy from 'lodash/uniqBy'
import HcPostCard from '~/components/PostCard'
import HcLoadMore from '~/components/LoadMore.vue'
import { mapGetters } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
import { filterPosts } from '~/graphql/PostQuery.js'
export default {
components: {
@ -49,7 +49,6 @@ export default {
const { hashtag = null } = this.$route.query
return {
// Initialize your apollo data
Post: [],
page: 1,
pageSize: 12,
filter: {},
@ -61,18 +60,27 @@ export default {
this.changeFilterBubble({ tags_some: { name: this.hashtag } })
}
},
watch: {
Post(post) {
this.setPosts(this.Post)
},
},
computed: {
...mapGetters({
currentUser: 'auth/user',
posts: 'posts/posts',
}),
tags() {
return this.Post ? this.Post[0].tags.map(tag => tag.name) : '-'
return this.posts ? this.posts.tags.map(tag => tag.name) : '-'
},
offset() {
return (this.page - 1) * this.pageSize
},
},
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
changeFilterBubble(filter) {
if (this.hashtag) {
filter = {
@ -129,47 +137,7 @@ export default {
apollo: {
Post: {
query() {
return gql(`
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
Post(filter: $filter, first: $first, offset: $offset) {
id
title
contentExcerpt
createdAt
disabled
deleted
slug
image
author {
id
avatar
slug
name
disabled
deleted
contributionsCount
shoutedCount
commentsCount
followedByCount
followedByCurrentUser
location {
name: name${this.$i18n.locale().toUpperCase()}
}
badges {
id
icon
}
}
commentsCount
categories {
id
name
icon
}
shoutedCount
}
}
`)
return filterPosts(this.$i18n)
},
variables() {
return {

76
webapp/store/posts.js Normal file
View File

@ -0,0 +1,76 @@
import gql from 'graphql-tag'
export const state = () => {
return {
posts: [],
}
}
export const mutations = {
SET_POSTS(state, posts) {
state.posts = posts || null
},
}
export const getters = {
posts(state) {
return state.posts || []
},
}
export const actions = {
async fetchPosts({ commit, dispatch }, { i18n, filter }) {
const client = this.app.apolloProvider.defaultClient
const {
data: { Post },
} = await client.query({
query: gql(`
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
Post(filter: $filter, first: $first, offset: $offset) {
id
title
contentExcerpt
createdAt
disabled
deleted
slug
image
author {
id
avatar
slug
name
disabled
deleted
contributionsCount
shoutedCount
commentsCount
followedByCount
followedByCurrentUser
location {
name: name${i18n.locale().toUpperCase()}
}
badges {
id
icon
}
}
commentsCount
categories {
id
name
icon
}
shoutedCount
}
}`),
variables: {
filter,
first: 12,
offset: 0,
},
})
commit('SET_POSTS', Post)
return Post
},
}