mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #969 from Human-Connection/967-filter-post-by-category
Filter posts by category
This commit is contained in:
commit
b97f7e464f
@ -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,
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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");
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
134
webapp/components/FilterPosts/FilterPosts.spec.js
Normal file
134
webapp/components/FilterPosts/FilterPosts.spec.js
Normal 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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
61
webapp/components/FilterPosts/FilterPosts.vue
Normal file
61
webapp/components/FilterPosts/FilterPosts.vue
Normal 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>
|
||||
126
webapp/components/FilterPosts/FilterPostsMenuItems.vue
Normal file
126
webapp/components/FilterPosts/FilterPostsMenuItems.vue
Normal 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>
|
||||
@ -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
|
||||
|
||||
11
webapp/graphql/CategoryQuery.js
Normal file
11
webapp/graphql/CategoryQuery.js
Normal file
@ -0,0 +1,11 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default () => {
|
||||
return gql(`{
|
||||
Category {
|
||||
id
|
||||
name
|
||||
icon
|
||||
}
|
||||
}`)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
"hashtag-search": "Suche nach #{hashtag}",
|
||||
"clearSearch": "Suche löschen"
|
||||
},
|
||||
"filter-posts": {
|
||||
"header": "Themenkategorien",
|
||||
"all": "Alle"
|
||||
},
|
||||
"site": {
|
||||
"made": "Mit ❤ 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>"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
"hashtag-search": "Searching for #{hashtag}",
|
||||
"clearSearch": "Clear search"
|
||||
},
|
||||
"filter-posts": {
|
||||
"header": "Categories of Content",
|
||||
"all": "All"
|
||||
},
|
||||
"site": {
|
||||
"made": "Made with ❤",
|
||||
"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": {
|
||||
|
||||
@ -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
76
webapp/store/posts.js
Normal 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
|
||||
},
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user