Merge branch '5059-epic-groups' into 5344-add-group-members-management
@ -4,7 +4,7 @@ export const CATEGORIES_MAX = 3
|
||||
|
||||
export const categories = [
|
||||
{
|
||||
icon: 'users',
|
||||
icon: 'networking',
|
||||
name: 'networking',
|
||||
description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe',
|
||||
},
|
||||
@ -14,12 +14,12 @@ export const categories = [
|
||||
description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten',
|
||||
},
|
||||
{
|
||||
icon: 'lightbulb',
|
||||
icon: 'energy',
|
||||
name: 'energy',
|
||||
description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...',
|
||||
},
|
||||
{
|
||||
icon: 'smile',
|
||||
icon: 'psyche',
|
||||
name: 'psyche',
|
||||
description: 'Seele, Gefühle, Glück',
|
||||
},
|
||||
@ -34,7 +34,7 @@ export const categories = [
|
||||
description: 'Menschenrechte, Gesetze, Verordnungen',
|
||||
},
|
||||
{
|
||||
icon: 'money',
|
||||
icon: 'finance',
|
||||
name: 'finance',
|
||||
description: 'Geld, Finanzsystem, Alternativwährungen, ...',
|
||||
},
|
||||
@ -44,7 +44,7 @@ export const categories = [
|
||||
description: 'Familie, Pädagogik, Schule, Prägung',
|
||||
},
|
||||
{
|
||||
icon: 'suitcase',
|
||||
icon: 'mobility',
|
||||
name: 'mobility',
|
||||
description: 'Reise, Verkehr, Elektromobilität',
|
||||
},
|
||||
@ -54,48 +54,48 @@ export const categories = [
|
||||
description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...',
|
||||
},
|
||||
{
|
||||
icon: 'angellist',
|
||||
icon: 'peace',
|
||||
name: 'peace',
|
||||
description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken',
|
||||
},
|
||||
{
|
||||
icon: 'university',
|
||||
icon: 'politics',
|
||||
name: 'politics',
|
||||
description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien',
|
||||
},
|
||||
{
|
||||
icon: 'tree',
|
||||
icon: 'nature',
|
||||
name: 'nature',
|
||||
description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt',
|
||||
},
|
||||
{
|
||||
icon: 'graduation-cap',
|
||||
icon: 'science',
|
||||
name: 'science',
|
||||
description: 'Bildung, Hochschule, Publikationen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'medkit',
|
||||
icon: 'health',
|
||||
name: 'health',
|
||||
description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...',
|
||||
},
|
||||
{
|
||||
icon: 'desktop',
|
||||
icon: 'media',
|
||||
name: 'it-and-media',
|
||||
description:
|
||||
'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps',
|
||||
},
|
||||
{
|
||||
icon: 'heart-o',
|
||||
icon: 'spirituality',
|
||||
name: 'spirituality',
|
||||
description: 'Religion, Werte, Ethik',
|
||||
},
|
||||
{
|
||||
icon: 'music',
|
||||
icon: 'culture',
|
||||
name: 'culture',
|
||||
description: 'Kunst, Theater, Musik, Fotografie, Film',
|
||||
},
|
||||
{
|
||||
icon: 'ellipsis-h',
|
||||
icon: 'miscellaneous',
|
||||
name: 'miscellaneous',
|
||||
description: '',
|
||||
},
|
||||
|
||||
@ -106,6 +106,17 @@ export const joinGroupMutation = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const leaveGroupMutation = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!) {
|
||||
LeaveGroup(groupId: $groupId, userId: $userId) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const changeGroupMemberRoleMutation = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
|
||||
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
|
||||
@ -120,36 +131,8 @@ export const changeGroupMemberRoleMutation = gql`
|
||||
// ------ queries
|
||||
|
||||
export const groupQuery = gql`
|
||||
query (
|
||||
$isMember: Boolean
|
||||
$id: ID
|
||||
$name: String
|
||||
$slug: String
|
||||
$createdAt: String
|
||||
$updatedAt: String
|
||||
$about: String
|
||||
$description: String
|
||||
$locationName: String
|
||||
$first: Int
|
||||
$offset: Int
|
||||
$orderBy: [_GroupOrdering]
|
||||
$filter: _GroupFilter
|
||||
) {
|
||||
Group(
|
||||
isMember: $isMember
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
createdAt: $createdAt
|
||||
updatedAt: $updatedAt
|
||||
about: $about
|
||||
description: $description
|
||||
locationName: $locationName
|
||||
first: $first
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
filter: $filter
|
||||
) {
|
||||
query ($isMember: Boolean, $id: ID, $slug: String) {
|
||||
Group(isMember: $isMember, id: $id, slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
@ -178,8 +161,8 @@ export const groupQuery = gql`
|
||||
`
|
||||
|
||||
export const groupMembersQuery = gql`
|
||||
query ($id: ID!, $first: Int, $offset: Int, $orderBy: [_UserOrdering], $filter: _UserFilter) {
|
||||
GroupMembers(id: $id, first: $first, offset: $offset, orderBy: $orderBy, filter: $filter) {
|
||||
query ($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
|
||||
@ -191,6 +191,36 @@ const isAllowedToJoinGroup = rule({
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedToLeaveGroup = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { groupId, userId } = args
|
||||
if (user.id !== userId) return false
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, userId },
|
||||
)
|
||||
return {
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { group, member } = await readTxPromise
|
||||
return !!group && !!member && !!member.myRoleInGroup && member.myRoleInGroup !== 'owner'
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAuthor = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
@ -284,6 +314,7 @@ export default shield(
|
||||
CreateGroup: isAuthenticated,
|
||||
UpdateGroup: isAllowedToChangeGroupSettings,
|
||||
JoinGroup: isAllowedToJoinGroup,
|
||||
LeaveGroup: isAllowedToLeaveGroup,
|
||||
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
|
||||
CreatePost: isAuthenticated,
|
||||
UpdatePost: isAuthor,
|
||||
|
||||
@ -4,21 +4,26 @@ import CONFIG from '../../config'
|
||||
import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories'
|
||||
import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups'
|
||||
import { removeHtmlTags } from '../../middleware/helpers/cleanHtml.js'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import Resolver, {
|
||||
removeUndefinedNullValuesFromObject,
|
||||
convertObjectToCypherMapLiteral,
|
||||
} from './helpers/Resolver'
|
||||
import { mergeImage } from './images/images'
|
||||
import createOrUpdateLocations from './users/location'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Group: async (_object, params, context, _resolveInfo) => {
|
||||
const { id: groupId, isMember } = params
|
||||
const { isMember, id, slug } = params
|
||||
const matchParams = { id, slug }
|
||||
removeUndefinedNullValuesFromObject(matchParams)
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const groupIdCypher = groupId ? ` {id: "${groupId}"}` : ''
|
||||
const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams, true)
|
||||
let groupCypher
|
||||
if (isMember === true) {
|
||||
groupCypher = `
|
||||
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupIdCypher})
|
||||
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupMatchParamsCypher})
|
||||
WITH group, membership
|
||||
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
@ -26,7 +31,7 @@ export default {
|
||||
} else {
|
||||
if (isMember === false) {
|
||||
groupCypher = `
|
||||
MATCH (group:Group${groupIdCypher})
|
||||
MATCH (group:Group${groupMatchParamsCypher})
|
||||
WHERE (NOT (:User {id: $userId})-[:MEMBER_OF]->(group))
|
||||
WITH group
|
||||
WHERE group.groupType IN ['public', 'closed']
|
||||
@ -34,7 +39,7 @@ export default {
|
||||
`
|
||||
} else {
|
||||
groupCypher = `
|
||||
MATCH (group:Group${groupIdCypher})
|
||||
MATCH (group:Group${groupMatchParamsCypher})
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
WITH group, membership
|
||||
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||
@ -244,6 +249,27 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
LeaveGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { groupId, userId } = params
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const leaveGroupCypher = `
|
||||
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
DELETE membership
|
||||
RETURN member {.*, myRoleInGroup: NULL}
|
||||
`
|
||||
const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId })
|
||||
const [member] = await transactionResponse.records.map((record) => record.get('member'))
|
||||
return member
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => {
|
||||
const { groupId, userId, roleInGroup } = params
|
||||
const session = context.driver.session()
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
createGroupMutation,
|
||||
updateGroupMutation,
|
||||
joinGroupMutation,
|
||||
leaveGroupMutation,
|
||||
changeGroupMemberRoleMutation,
|
||||
groupMembersQuery,
|
||||
groupQuery,
|
||||
@ -17,6 +18,12 @@ const neode = getNeode()
|
||||
|
||||
let authenticatedUser
|
||||
let user
|
||||
let noMemberUser
|
||||
let pendingMemberUser
|
||||
let usualMemberUser
|
||||
let adminMemberUser
|
||||
let ownerMemberUser
|
||||
let secondOwnerMemberUser
|
||||
|
||||
const categoryIds = ['cat9', 'cat4', 'cat15']
|
||||
const descriptionAdditional100 =
|
||||
@ -76,6 +83,169 @@ const seedBasicsAndClearAuthentication = async () => {
|
||||
authenticatedUser = null
|
||||
}
|
||||
|
||||
const seedComplexScenarioAndClearAuthentication = async () => {
|
||||
await seedBasicsAndClearAuthentication()
|
||||
// create users
|
||||
noMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'none-member-user',
|
||||
name: 'None Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'none-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
pendingMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'pending-member-user',
|
||||
name: 'Pending Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'pending-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
usualMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'usual-member-user',
|
||||
name: 'Usual Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'usual-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
adminMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'admin-member-user',
|
||||
name: 'Admin Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'admin-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
ownerMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'owner-member-user',
|
||||
name: 'Owner Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'owner-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
secondOwnerMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'second-owner-member-user',
|
||||
name: 'Second Owner Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'second-owner-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
// create groups
|
||||
// public-group
|
||||
authenticatedUser = await usualMemberUser.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'public-group',
|
||||
name: 'The Best Group',
|
||||
about: 'We will change the world!',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'public',
|
||||
actionRadius: 'regional',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'public-group',
|
||||
userId: 'owner-of-closed-group',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'public-group',
|
||||
userId: 'owner-of-hidden-group',
|
||||
},
|
||||
})
|
||||
// closed-group
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'closed-group',
|
||||
name: 'Uninteresting Group',
|
||||
about: 'We will change nothing!',
|
||||
description: 'We love it like it is!?' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
// hidden-group
|
||||
authenticatedUser = await adminMemberUser.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'hidden-group',
|
||||
name: 'Investigative Journalism Group',
|
||||
about: 'We will change all.',
|
||||
description: 'We research …' + descriptionAdditional100,
|
||||
groupType: 'hidden',
|
||||
actionRadius: 'global',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
// 'JoinGroup' mutation does not work in hidden groups so we join them by 'ChangeGroupMemberRole' through the owner
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'admin-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'second-owner-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'admin-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'second-owner-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
|
||||
authenticatedUser = null
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -503,6 +673,72 @@ describe('in mode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('with given slug', () => {
|
||||
describe("slug = 'the-best-group'", () => {
|
||||
it('finds only the listed group with this slug', async () => {
|
||||
const result = await query({
|
||||
query: groupQuery,
|
||||
variables: { slug: 'the-best-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: [
|
||||
expect.objectContaining({
|
||||
id: 'my-group',
|
||||
slug: 'the-best-group',
|
||||
myRole: 'owner',
|
||||
}),
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
expect(result.data.Group.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("slug = 'third-investigative-journalism-group'", () => {
|
||||
it("finds only the hidden group where I'm 'usual' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery,
|
||||
variables: { slug: 'third-investigative-journalism-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'third-hidden-group',
|
||||
slug: 'third-investigative-journalism-group',
|
||||
myRole: 'usual',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
expect(result.data.Group.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("slug = 'second-investigative-journalism-group'", () => {
|
||||
it("finds no hidden group where I'm 'pending' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery,
|
||||
variables: { slug: 'second-investigative-journalism-group' },
|
||||
})
|
||||
expect(result.data.Group.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("slug = 'investigative-journalism-group'", () => {
|
||||
it("finds no hidden group where I'm not(!) a member at all", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery,
|
||||
variables: { slug: 'investigative-journalism-group' },
|
||||
})
|
||||
expect(result.data.Group.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isMember = true', () => {
|
||||
it('finds only listed groups where user is member', async () => {
|
||||
const result = await query({ query: groupQuery, variables: { isMember: true } })
|
||||
@ -966,8 +1202,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -999,8 +1235,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1032,8 +1268,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1075,8 +1311,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1108,8 +1344,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1173,8 +1409,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1210,8 +1446,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1247,8 +1483,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await mutate({
|
||||
mutation: groupMembersQuery,
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1305,162 +1541,8 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
describe('ChangeGroupMemberRole', () => {
|
||||
let pendingMemberUser
|
||||
let usualMemberUser
|
||||
let adminMemberUser
|
||||
let ownerMemberUser
|
||||
let secondOwnerMemberUser
|
||||
|
||||
beforeAll(async () => {
|
||||
await seedBasicsAndClearAuthentication()
|
||||
// create users
|
||||
pendingMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'pending-member-user',
|
||||
name: 'Pending Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'pending-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
usualMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'usual-member-user',
|
||||
name: 'Usual Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'usual-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
adminMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'admin-member-user',
|
||||
name: 'Admin Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'admin-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
ownerMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'owner-member-user',
|
||||
name: 'Owner Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'owner-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
secondOwnerMemberUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'second-owner-member-user',
|
||||
name: 'Second Owner Member TestUser',
|
||||
},
|
||||
{
|
||||
email: 'second-owner-member-user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
// create groups
|
||||
// public-group
|
||||
authenticatedUser = await usualMemberUser.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'public-group',
|
||||
name: 'The Best Group',
|
||||
about: 'We will change the world!',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'public',
|
||||
actionRadius: 'regional',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'public-group',
|
||||
userId: 'owner-of-closed-group',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'public-group',
|
||||
userId: 'owner-of-hidden-group',
|
||||
},
|
||||
})
|
||||
// closed-group
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'closed-group',
|
||||
name: 'Uninteresting Group',
|
||||
about: 'We will change nothing!',
|
||||
description: 'We love it like it is!?' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
// hidden-group
|
||||
authenticatedUser = await adminMemberUser.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'hidden-group',
|
||||
name: 'Investigative Journalism Group',
|
||||
about: 'We will change all.',
|
||||
description: 'We research …' + descriptionAdditional100,
|
||||
groupType: 'hidden',
|
||||
actionRadius: 'global',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
// 'JoinGroup' mutation does not work in hidden groups so we join them by 'ChangeGroupMemberRole' through the owner
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'admin-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'second-owner-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'admin-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'hidden-group',
|
||||
userId: 'second-owner-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
|
||||
authenticatedUser = null
|
||||
await seedComplexScenarioAndClearAuthentication()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -2264,6 +2346,241 @@ describe('in mode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('LeaveGroup', () => {
|
||||
beforeAll(async () => {
|
||||
await seedComplexScenarioAndClearAuthentication()
|
||||
// closed-group
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'closed-group',
|
||||
userId: 'pending-member-user',
|
||||
roleInGroup: 'pending',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'closed-group',
|
||||
userId: 'usual-member-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'closed-group',
|
||||
userId: 'admin-member-user',
|
||||
roleInGroup: 'admin',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'closed-group',
|
||||
userId: 'second-owner-member-user',
|
||||
roleInGroup: 'owner',
|
||||
},
|
||||
})
|
||||
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
groupId: 'not-existing-group',
|
||||
userId: 'current-user',
|
||||
},
|
||||
})
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
describe('in all group types', () => {
|
||||
describe('here "closed-group" for example', () => {
|
||||
const memberInGroup = async (userId, groupId) => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery,
|
||||
variables: {
|
||||
id: groupId,
|
||||
},
|
||||
})
|
||||
return result.data && result.data.GroupMembers
|
||||
? !!result.data.GroupMembers.find((member) => member.id === userId)
|
||||
: null
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = null
|
||||
variables = {
|
||||
groupId: 'closed-group',
|
||||
}
|
||||
})
|
||||
|
||||
describe('left by "pending-member-user"', () => {
|
||||
it('has "null" as membership role, was in the group, and left the group', async () => {
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
expect(await memberInGroup('pending-member-user', 'closed-group')).toBe(true)
|
||||
authenticatedUser = await pendingMemberUser.toJson()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'pending-member-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
LeaveGroup: {
|
||||
id: 'pending-member-user',
|
||||
myRoleInGroup: null,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
expect(await memberInGroup('pending-member-user', 'closed-group')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('left by "usual-member-user"', () => {
|
||||
it('has "null" as membership role, was in the group, and left the group', async () => {
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
expect(await memberInGroup('usual-member-user', 'closed-group')).toBe(true)
|
||||
authenticatedUser = await usualMemberUser.toJson()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'usual-member-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
LeaveGroup: {
|
||||
id: 'usual-member-user',
|
||||
myRoleInGroup: null,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
expect(await memberInGroup('usual-member-user', 'closed-group')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('left by "admin-member-user"', () => {
|
||||
it('has "null" as membership role, was in the group, and left the group', async () => {
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
expect(await memberInGroup('admin-member-user', 'closed-group')).toBe(true)
|
||||
authenticatedUser = await adminMemberUser.toJson()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'admin-member-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
LeaveGroup: {
|
||||
id: 'admin-member-user',
|
||||
myRoleInGroup: null,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
expect(await memberInGroup('admin-member-user', 'closed-group')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('left by "owner-member-user"', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
const { errors } = await mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'owner-member-user',
|
||||
},
|
||||
})
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('left by "second-owner-member-user"', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = await secondOwnerMemberUser.toJson()
|
||||
const { errors } = await mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'second-owner-member-user',
|
||||
},
|
||||
})
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('left by "none-member-user"', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = await noMemberUser.toJson()
|
||||
const { errors } = await mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'none-member-user',
|
||||
},
|
||||
})
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as "owner-member-user" try to leave member "usual-member-user"', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = await ownerMemberUser.toJson()
|
||||
const { errors } = await mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'usual-member-user',
|
||||
},
|
||||
})
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as "usual-member-user" try to leave member "admin-member-user"', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = await usualMemberUser.toJson()
|
||||
const { errors } = await mutate({
|
||||
mutation: leaveGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
userId: 'admin-member-user',
|
||||
},
|
||||
})
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateGroup', () => {
|
||||
beforeAll(async () => {
|
||||
await seedBasicsAndClearAuthentication()
|
||||
|
||||
@ -121,3 +121,25 @@ export default function Resolver(type, options = {}) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const removeUndefinedNullValuesFromObject = (obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if ([undefined, null].includes(obj[key])) {
|
||||
delete obj[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const convertObjectToCypherMapLiteral = (params, addSpaceInfrontIfMapIsNotEmpty = false) => {
|
||||
// I have found no other way yet. maybe "apoc.convert.fromJsonMap(key)" can help, but couldn't get it how, see: https://stackoverflow.com/questions/43217823/neo4j-cypher-inline-conversion-of-string-to-a-map
|
||||
// result looks like: '{id: "g0", slug: "yoga"}'
|
||||
const paramsEntries = Object.entries(params)
|
||||
let mapLiteral = ''
|
||||
paramsEntries.forEach((ele, index) => {
|
||||
mapLiteral += index === 0 ? '{' : ''
|
||||
mapLiteral += `${ele[0]}: "${ele[1]}"`
|
||||
mapLiteral += index < paramsEntries.length - 1 ? ', ' : '}'
|
||||
})
|
||||
mapLiteral = (addSpaceInfrontIfMapIsNotEmpty && mapLiteral.length > 0 ? ' ' : '') + mapLiteral
|
||||
return mapLiteral
|
||||
}
|
||||
|
||||
@ -20,16 +20,22 @@ export default {
|
||||
const result = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $id})
|
||||
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] as media
|
||||
RETURN user {.*, socialMedia: media } as user
|
||||
OPTIONAL MATCH (category:Category) WHERE NOT ((user)-[:NOT_INTERESTED_IN]->(category))
|
||||
OPTIONAL MATCH (cats:Category)
|
||||
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] AS media, category, toString(COUNT(cats)) AS categoryCount
|
||||
RETURN user {.*, socialMedia: media, activeCategories: collect(category.id) } AS user, categoryCount
|
||||
`,
|
||||
{ id: user.id },
|
||||
)
|
||||
log(result)
|
||||
return result.records.map((record) => record.get('user'))
|
||||
const [categoryCount] = result.records.map((record) => record.get('categoryCount'))
|
||||
const [currentUser] = result.records.map((record) => record.get('user'))
|
||||
// frontend expects empty array when all categories are selected
|
||||
if (currentUser.activeCategories.length === parseInt(categoryCount))
|
||||
currentUser.activeCategories = []
|
||||
return currentUser
|
||||
})
|
||||
try {
|
||||
const [currentUser] = await currentUserTransactionPromise
|
||||
const currentUser = await currentUserTransactionPromise
|
||||
return currentUser
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -7,6 +7,7 @@ import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer, { context } from '../../server'
|
||||
import encode from '../../jwt/encode'
|
||||
import { getNeode } from '../../db/neo4j'
|
||||
import { categories } from '../../constants/categories'
|
||||
|
||||
const neode = getNeode()
|
||||
let query, mutate, variables, req, user
|
||||
@ -119,6 +120,7 @@ describe('currentUser', () => {
|
||||
}
|
||||
email
|
||||
role
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -173,6 +175,52 @@ describe('currentUser', () => {
|
||||
}
|
||||
await respondsWith(expected)
|
||||
})
|
||||
|
||||
describe('with categories in DB', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
categories.map(async ({ icon, name }, index) => {
|
||||
await Factory.build('category', {
|
||||
id: `cat${index + 1}`,
|
||||
slug: name,
|
||||
name,
|
||||
icon,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty array for all categories', async () => {
|
||||
await respondsWith({
|
||||
data: {
|
||||
currentUser: expect.objectContaining({ activeCategories: [] }),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('with categories saved for current user', () => {
|
||||
const saveCategorySettings = gql`
|
||||
mutation ($activeCategories: [String]) {
|
||||
saveCategorySettings(activeCategories: $activeCategories)
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: saveCategorySettings,
|
||||
variables: { activeCategories: ['cat1', 'cat3', 'cat5', 'cat7'] },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns only the saved active categories', async () => {
|
||||
const result = await query({ query: currentUserQuery, variables })
|
||||
expect(result.data.currentUser.activeCategories).toHaveLength(4)
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat1')
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat3')
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat5')
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat7')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -286,6 +286,10 @@ export default {
|
||||
{ id },
|
||||
)
|
||||
})
|
||||
|
||||
// frontend gives [] when all categories are selected (default)
|
||||
if (activeCategories.length === 0) return true
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const saveCategorySettingsResponse = await transaction.run(
|
||||
`
|
||||
|
||||
@ -602,9 +602,7 @@ describe('save category settings', () => {
|
||||
const userQuery = gql`
|
||||
query ($id: ID) {
|
||||
User(id: $id) {
|
||||
activeCategories {
|
||||
id
|
||||
}
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -631,11 +629,7 @@ describe('save category settings', () => {
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining([
|
||||
{ id: 'cat1' },
|
||||
{ id: 'cat3' },
|
||||
{ id: 'cat5' },
|
||||
]),
|
||||
activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']),
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -678,11 +672,11 @@ describe('save category settings', () => {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining([
|
||||
{ id: 'cat10' },
|
||||
{ id: 'cat11' },
|
||||
{ id: 'cat12' },
|
||||
{ id: 'cat8' },
|
||||
{ id: 'cat9' },
|
||||
'cat10',
|
||||
'cat11',
|
||||
'cat12',
|
||||
'cat8',
|
||||
'cat9',
|
||||
]),
|
||||
},
|
||||
],
|
||||
|
||||
@ -62,27 +62,19 @@ type Query {
|
||||
Group(
|
||||
isMember: Boolean # if 'undefined' or 'null' then get all groups
|
||||
id: ID
|
||||
name: String
|
||||
slug: String
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
about: String
|
||||
description: String
|
||||
# groupType: GroupType # test this
|
||||
# actionRadius: GroupActionRadius # test this
|
||||
# avatar: ImageInput # test this
|
||||
locationName: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_GroupOrdering]
|
||||
# first: Int # not implemented yet
|
||||
# offset: Int # not implemented yet
|
||||
# orderBy: [_GroupOrdering] # not implemented yet
|
||||
# filter: _GroupFilter # not implemented yet
|
||||
): [Group]
|
||||
|
||||
GroupMembers(
|
||||
id: ID!
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_UserOrdering]
|
||||
filter: _UserFilter
|
||||
# first: Int # not implemented yet
|
||||
# offset: Int # not implemented yet
|
||||
# orderBy: [_UserOrdering] # not implemented yet
|
||||
# filter: _UserFilter # not implemented yet
|
||||
): [User]
|
||||
|
||||
# AvailableGroupTypes: [GroupType]!
|
||||
@ -126,6 +118,11 @@ type Mutation {
|
||||
userId: ID!
|
||||
): User
|
||||
|
||||
LeaveGroup(
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
): User
|
||||
|
||||
ChangeGroupMemberRole(
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
|
||||
@ -115,11 +115,11 @@ type User {
|
||||
|
||||
emotions: [EMOTED]
|
||||
|
||||
activeCategories: [Category] @cypher(
|
||||
activeCategories: [String] @cypher(
|
||||
statement: """
|
||||
MATCH (category:Category)
|
||||
WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category))
|
||||
RETURN category
|
||||
RETURN collect(category.id)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mock-socket": "^9.0.3",
|
||||
"neo4j-driver": "^4.3.4",
|
||||
"neode": "^0.4.7",
|
||||
"neode": "^0.4.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rosie": "^2.1.0",
|
||||
"slug": "^6.0.0"
|
||||
|
||||
20
webapp/assets/_new/icons/svgs/culture.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M5.9,25.1c-0.3,0.3-0.3,0.8,0,1.1s0.8,0.3,1.1,0c1.7-1.7,4.5-1.7,6.3,0c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2
|
||||
c0.3-0.3,0.3-0.8,0-1.1C12,22.8,8.2,22.8,5.9,25.1z"/>
|
||||
<path d="M24.4,8.7c0.7-0.7,2-0.7,2.7,0C27.2,8.9,27.4,9,27.6,9c0.2,0,0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1
|
||||
c-1.3-1.3-3.5-1.3-4.8,0C23,8,23,8.5,23.3,8.7C23.6,9,24.1,9,24.4,8.7z"/>
|
||||
<path d="M16.4,7.7c-0.3,0.3-0.3,0.8,0,1.1c0.3,0.3,0.8,0.3,1.1,0c0.7-0.7,1.9-0.7,2.7,0C20.3,8.9,20.5,9,20.7,9s0.4-0.1,0.5-0.2
|
||||
c0.3-0.3,0.3-0.8,0-1.1C19.9,6.4,17.7,6.4,16.4,7.7z"/>
|
||||
<path d="M31.4,0.8c-0.2-0.1-0.5-0.2-0.7-0.1c-2,0.8-5.1,1.2-8.4,1.2s-6.4-0.4-8.4-1.2c-0.2-0.1-0.5-0.1-0.7,0.1
|
||||
c-0.2,0.1-0.3,0.4-0.3,0.6v9.9c-1,0.1-1.9,0.1-3,0.1c-3.3,0-6.4-0.4-8.4-1.2c-0.2-0.1-0.5-0.1-0.7,0.1c-0.2,0.1-0.3,0.4-0.3,0.6
|
||||
l0,11.5c0,5.2,4.2,9.4,9.4,9.4s9.4-4.2,9.4-9.4V22c0.7,0.3,1.6,0.4,3,0.4c5.2,0,9.4-4.2,9.4-9.4V1.4C31.7,1.2,31.6,0.9,31.4,0.8z
|
||||
M9.9,30.4c-4.4,0-7.9-3.6-7.9-7.9V12c2.1,0.6,4.9,1,7.9,1c2.7,0,5.2-0.3,7.3-0.8l0.5-0.1c0,1.6,0,2.9,0,4c0,1.3,0,2.4,0.1,3.2v3.2
|
||||
C17.8,26.8,14.2,30.4,9.9,30.4z M30.2,13c0,4.4-3.6,7.9-7.9,7.9c-2.1,0-2.8,0-3-1.7v-2.8c0.9,0.5,1.9,0.8,3,0.8
|
||||
c1.6,0,3.1-0.6,4.2-1.7c0.3-0.3,0.3-0.8,0-1.1s-0.8-0.3-1.1,0c-0.8,0.8-2,1.3-3.1,1.3c-1.1,0-2.1-0.4-3-1.2v-3.6
|
||||
c0-0.2-0.1-0.5-0.3-0.6c-0.2-0.1-0.5-0.2-0.7-0.1c-0.4,0.2-0.9,0.3-1.4,0.4l-2.5,0.4V2.5c2.1,0.6,4.9,1,7.9,1c3,0,5.8-0.3,7.9-1V13
|
||||
z"/>
|
||||
<path d="M10.9,17.7c-0.3,0.3-0.3,0.8,0,1.1s0.8,0.3,1.1,0c0.7-0.7,2-0.7,2.7,0c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2
|
||||
c0.3-0.3,0.3-0.8,0-1.1C14.4,16.4,12.2,16.4,10.9,17.7z"/>
|
||||
<path d="M7.7,18.7C7.9,18.9,8.1,19,8.3,19s0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1c-1.3-1.3-3.5-1.3-4.8,0c-0.3,0.3-0.3,0.8,0,1.1
|
||||
s0.8,0.3,1.1,0C5.8,18,7,18,7.7,18.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
14
webapp/assets/_new/icons/svgs/energy.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M27.6,19.7L26.8,19c-3-2.8-4.8-4.4-5.2-4.6c-0.9-0.6-2.9-0.5-3.6-0.4c-0.2-0.5-0.5-0.9-0.9-1.2V2.2
|
||||
c0-1-0.7-1.6-1.3-1.6c-0.6-0.1-1.4,0.4-1.6,1.4c-0.1,0.5-0.3,1.4-0.5,2.4c-0.6,2.9-1,5-1,5.4c-0.1,1.1,1.1,2.8,1.5,3.3
|
||||
c-0.3,0.4-0.5,0.9-0.5,1.4c0,0,0,0,0,0.1c-0.9,0.6-7.3,5-8.7,6c-0.8,0.6-0.9,1.4-0.6,2c0.2,0.4,0.7,0.7,1.2,0.7
|
||||
c0.3,0,0.5-0.1,0.8-0.2c1.5-0.6,6.7-2.9,7.4-3.2c0.2-0.1,0.4-0.3,0.6-0.5l-1.3,11c0,1,2.3,1,2.8,1s2.8,0,2.8-1.1l-1.4-13.5
|
||||
c1.8,1.1,7.2,4.4,8.6,5.2c0.3,0.2,0.7,0.3,0.9,0.3c0.5,0,0.9-0.2,1.1-0.5C28.5,21.4,28.5,20.5,27.6,19.7z M14.8,14.6
|
||||
c0-0.8,0.6-1.2,1.2-1.2c0.6,0,1.2,0.6,1.2,1.2c0,0.8-0.6,1.2-1.2,1.2C15.2,15.8,14.8,15.2,14.8,14.6z M14.8,4.7c0.2-1,0.4-2,0.5-2.5
|
||||
c0.1-0.5,0.3-0.7,0.5-0.6c0.2,0,0.4,0.2,0.4,0.6v10.2c-0.1,0-0.1,0-0.2,0c-0.3,0-0.6,0.1-0.9,0.2c-0.6-0.8-1.4-2-1.3-2.7
|
||||
C13.8,9.5,14.4,6.5,14.8,4.7z M13.5,19.1c-0.6,0.3-5.8,2.6-7.3,3.2c-0.4,0.2-0.7,0.1-0.8-0.1c-0.1-0.2,0-0.4,0.3-0.7
|
||||
c1.3-0.9,6.9-4.8,8.4-5.8c0.2,0.3,0.5,0.6,0.8,0.8C14.6,17.4,13.9,18.8,13.5,19.1z M16,30.4c-0.6,0-1.4-0.1-1.8-0.2l1.6-13.4
|
||||
c0,0,0.1,0,0.1,0c0.1,0,0.3,0,0.4,0l1.4,13.4C17.4,30.3,16.6,30.4,16,30.4z M27.3,21.3c-0.1,0.1-0.4,0.2-0.7,0
|
||||
c-1.5-0.9-7.2-4.4-8.8-5.3c0.2-0.3,0.3-0.6,0.4-0.9c1-0.1,2.5-0.1,3,0.3c0.4,0.2,3.6,3.2,5,4.5l0.8,0.7
|
||||
C27.3,20.8,27.4,21.1,27.3,21.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
13
webapp/assets/_new/icons/svgs/finance.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<rect x="8.1" y="13.3" width="1.5" height="1.9"/>
|
||||
<path d="M31.9,17.9L31.9,17.9l0-0.6c-0.2-5.5-4.1-10.3-9.5-11.6c-0.2,0-0.8-0.2-0.8-0.2c-3.1-0.6-6.3-0.5-9.5,0.1
|
||||
c-0.3,0.1-0.6,0.1-0.9,0.2C10.6,5.7,10,5.4,9.7,5.4L9.5,5.3C8.7,5,7.6,4.6,6.8,5.1C6,5.7,6,6.7,6.2,7.3l0.3,1.2
|
||||
C5.1,9.7,4,11.2,3.3,12.8c-0.4,0.8-0.8,0.8-1.2,0.8c-0.1,0-0.2,0-0.3,0H0l0,5.5l0.6,0.1c0,0,1.1,0.2,1.6,1C2.5,20.7,2.7,21,3,22.3
|
||||
c1.1,3.4,3.6,5.3,4.6,5.9l0,3.9H12l2.6-3.4h6.1l1.7,3.4h4.9v-4.6l0.4-0.4c2.5-2.2,3.9-5.2,4.1-8.5l0-0.4
|
||||
C31.9,18.1,31.9,18,31.9,17.9z M26.7,25.9l-0.9,0.9v3.8h-2.5l-1.7-3.4h-7.7l-2.6,3.4H9.1l0-3.2l-0.4-0.2c0,0-3.1-1.7-4.2-5.3
|
||||
c-0.4-1.4-0.6-1.8-1-2.3c-0.5-0.9-1.4-1.3-2-1.5v-2.8l0.6,0c0.5,0,1.8,0,2.6-1.7c0.5-1.2,1.3-2.3,2.2-3.3c0.8-0.8,1.3-1,1.1-1.5
|
||||
l0,0L7.6,7c-0.1-0.2-0.1-0.5,0-0.6c0.2-0.1,1,0.1,1.4,0.3l0.3,0.1c0.3,0.1,0.9,0.4,1.7,0.6l0.2,0.1l0.3-0.1c0.3-0.1,0.7-0.2,1-0.2
|
||||
c3-0.6,5.9-0.6,8.9-0.1c0,0,0.6,0.1,0.8,0.1c4.7,1.1,8.2,5.3,8.4,10.1l0,0.8c-0.1,1.3-1.2,2.4-2.5,2.4c-1,0-1.9-0.9-1.9-2
|
||||
c0-0.8,0.6-1.4,1.4-1.4v-1.5c-1.6,0-2.9,1.3-2.9,2.9c0,1.9,1.5,3.5,3.4,3.5c0.7,0,1.3-0.2,1.8-0.5C29.1,23.2,28.1,24.7,26.7,25.9z"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
7
webapp/assets/_new/icons/svgs/health.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M26.1,20.7c-2.9,5.1-9.3,8.8-10.8,9.6c-2.1-1.1-13.7-7.8-13.7-16.1c0-3.6,2.8-6.5,6.5-6.5c2.6,0,4.9,1.5,5.9,3.9l0.7,1.7
|
||||
l0.7-1.7c1-2.4,3.3-3.9,5.9-3.9c3.6,0,6.5,2.8,6.5,6.5V15h1.5v-0.8c0-4.5-3.5-8-8-8c-2.7,0-5.2,1.3-6.6,3.5
|
||||
c-1.4-2.2-3.9-3.5-6.6-3.5c-4.5,0-8,3.5-8,8c0,10,14.3,17.3,14.9,17.6l0.3,0.2l0.3-0.2c0.3-0.2,8.3-4.2,11.8-10.4l0.4-0.7L26.5,20
|
||||
L26.1,20.7z"/>
|
||||
<polygon points="22.9,17 19.5,21.2 17,14.5 11.6,19.9 12.6,20.9 16.4,17.2 19,24.2 23.6,18.5 31.9,18.5 31.9,17 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 625 B |
7
webapp/assets/_new/icons/svgs/media.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M31.7,29.2L29.8,23V7.1c0-1.1-1-2.1-2.1-2.1H4.8c-1.1,0-2.1,1-2.1,2.1V23l-2,6.1c-0.2,0.6-0.1,1.3,0.2,1.9
|
||||
c0.4,0.6,1,0.9,1.7,0.9h27.1c0.7,0,1.3-0.3,1.7-0.9C31.8,30.4,31.9,29.7,31.7,29.2z M4.2,7.1c0-0.3,0.3-0.6,0.6-0.6h22.8
|
||||
c0.3,0,0.6,0.3,0.6,0.6v15.3h-24V7.1z M30.2,30.2c-0.1,0.1-0.2,0.2-0.5,0.2H2.6c-0.3,0-0.4-0.2-0.5-0.2C2.1,30,2,29.8,2.1,29.6
|
||||
L4,23.9h24.4l1.8,5.8C30.4,29.8,30.3,30,30.2,30.2z"/>
|
||||
<rect x="13.5" y="27.8" width="5.6" height="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 596 B |
13
webapp/assets/_new/icons/svgs/miscellaneous.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M7.5,4.9c0.1-0.1,0.2-0.2,0.3-0.2l0.7-0.6l-1-1.4L6.8,3.1C6.7,3.3,6.6,3.4,6.5,3.5L5.8,4.1l1,1.3L7.5,4.9z"/>
|
||||
<path d="M31.5,19.2c0.2-1,0.3-2.1,0.3-3.2c0-8.6-7-15.8-15.8-15.8c-1.7,0-3.3,0.2-5,0.8l-0.8,0.2l0.6,1.6l0.8-0.2
|
||||
c1.5-0.5,3-0.7,4.5-0.7c3.6,0,6.9,1.4,9.4,3.7C22.7,6,21.3,7.3,21,9.7c-0.2,1.3,0.6,2.4,1.4,3.3c0.3,0.5,1,1.3,0.9,1.5
|
||||
c0,0.2-0.2,0.3-0.6,0.6c-0.8,0.6-2,1.3-1.7,3.6c0.2,1.6,0.9,2.8,2.2,3.1c0.2,0.1,0.6,0.1,0.8,0.1c1,0,2.1-0.6,2.9-1.5
|
||||
c0.8-1,0.8-1,2-0.5c0.2,0.1,0.4,0.2,0.6,0.3c-1.8,5.7-7.1,9.8-13.4,9.8c-1.7,0-3.4-0.3-4.9-0.9l-0.1-0.1c-0.7-1.8,1-4.3,2.9-5.9
|
||||
c2.1-1.8,2.9-4.6,2.1-7.1c-0.5-1.2-1.4-2.1-2.7-2.3C12,13.5,10.6,14,9.7,15c-1,1.3-1.8,2-3,2c-1.3-0.1-3.3-2-4.3-3.1l-0.2-0.3
|
||||
c0.4-2.1,1.2-4,2.4-5.7L5,7.2l-1.4-1L3.1,6.8c-1.8,2.8-2.9,5.9-2.9,9.2c0,8.6,7.1,15.8,15.9,15.8c7.3,0,13.5-5.1,15.2-11.9
|
||||
L31.5,19.2L31.5,19.2z M2,16.1c0,0,0-0.1,0-0.1c1.1,1.1,2.9,2.6,4.5,2.6c2.1,0.1,3.3-1.3,4.5-2.5c0.5-0.6,1.3-0.8,2.1-0.7
|
||||
c0.3,0.1,1,0.3,1.3,1.2c0.6,1.8,0,4-1.5,5.3c-0.9,0.9-3.7,3.6-3.5,6.5C5,26,2,21.3,2,16.1z M29.6,18.4c-1.7-0.9-2.7-1-4.1,0.9
|
||||
c-0.6,0.7-1.3,1-1.8,0.9s-0.8-0.7-0.9-1.6c-0.1-1.3,0.3-1.5,0.9-2c0.5-0.3,1-0.7,1.3-1.5c0.3-1.2-0.5-2.2-1.2-3.1
|
||||
c-0.5-0.7-1.2-1.5-1-2.1c0.1-1,0.3-2.6,4.3-2.7c1.9,2.4,3.1,5.5,3.1,8.8c0,0.8-0.1,1.6-0.2,2.4C29.8,18.4,29.7,18.4,29.6,18.4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
11
webapp/assets/_new/icons/svgs/mobility.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M31.8,20c0-6.8-5.6-9.1-8.2-9.1h-3.2c-0.3-1.6-1.7-2.9-3.5-2.9h-2.2v1.1h-3.4v1.5h3.4v2.1h-3.4v1.5h3.4v0.9h2.2
|
||||
c1.7,0,3.1-1.2,3.4-2.7h3.3c1.3,0,6.7,1.3,6.7,7.6c0,6.9-7.2,7.2-7.2,7.2h-0.8h-0.1V28c0,1.3-1.1,2.4-2.5,2.4
|
||||
c-1.4,0-2.5-1.1-2.5-2.4v-0.8H8.6V28c0,1.3-1.1,2.4-2.4,2.4c-1.5,0-2.4-1.2-2.4-2.4v-0.8H1.8v-2.6c0-0.1,0-0.8,0.5-1
|
||||
c0.5-0.1,1.3-0.4,2.2-0.7c0.5-0.2,1.1-0.4,1.2-0.4c0.4,0,0.8-0.3,1.4-1l0.1-0.1c0.1-0.1,0.5-0.7,0.9-1.1c0.8-0.9,1-1.2,1.1-1.4
|
||||
c0.1-0.1,0.5-0.4,1-0.4h8.6c0.6,0,0.8,0.2,1,0.4c0.4,0.5,1.9,2.5,1.9,2.5l1.2-0.9c0,0-1.5-2-1.9-2.5c-0.5-0.7-1.2-1-2.2-1h-8.6
|
||||
c-1,0-2,0.6-2.2,1.1c-0.1,0.1-0.6,0.8-1,1.2c-0.5,0.6-0.8,1-0.9,1.2c-0.2,0.2-0.5,0.4-0.6,0.6c-0.2,0-0.5,0.2-1.5,0.5
|
||||
c-0.7,0.3-1.6,0.6-2,0.7c-1,0.3-1.7,1.2-1.7,2.4v4.1h2.2c0.3,1.8,1.9,3.2,3.8,3.2c1.9,0,3.5-1.4,3.9-3.2h5.7
|
||||
c0.3,1.8,1.9,3.2,3.9,3.2c1.9,0,3.5-1.4,3.9-3.2C26.2,28.5,31.8,26.6,31.8,20z M16.9,13.6h-0.8V9.6h0.8c1.1,0,2.1,0.9,2.1,2
|
||||
S18,13.6,16.9,13.6z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,20 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="24px" height="24.3px" viewBox="0 0 24 24.3" style="enable-background:new 0 0 24 24.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#333333;}
|
||||
</style>
|
||||
<g>
|
||||
<circle class="st0" cx="17.1" cy="2.9" r="2.9"/>
|
||||
<path class="st0" d="M23.5,5.6L23.5,5.6c-0.6-0.5-1.4-0.5-2,0.1l-2.7,3l-2.2-1.4c0.1,0.3,0.1,0.6,0.1,0.9c0,0.6-0.2,1.1-0.5,1.6
|
||||
L16,10.2l1.5,1c0.4,0.3,0.9,0.4,1.4,0.4c0.1,0,0.2,0,0.4,0c0.6-0.1,1.2-0.4,1.6-0.9l2.7-3.1C24.2,7,24.1,6.1,23.5,5.6z"/>
|
||||
<path class="st0" d="M6.5,17c-0.3-0.2-0.5-0.5-0.7-0.8c0,0,0,0,0,0L4.6,19l-4,2.7C0,22.1-0.2,23,0.2,23.6l0,0
|
||||
c0.4,0.6,1.3,0.8,1.9,0.4l4.7-3.2l1.4-3.3l-0.7-0.1C7.2,17.4,6.8,17.2,6.5,17z"/>
|
||||
<path class="st0" d="M16.3,15.1l-4.4-0.5l3.5-4.7l0.3-0.4C16,9,16.2,8.6,16.2,8.2c0-0.7-0.4-1.4-1-1.7c-0.1,0-0.1-0.1-0.2-0.1
|
||||
l-1.8-0.7L9.4,4.4C9.1,4.3,8.8,4.3,8.5,4.3c-0.3,0-0.5,0-0.8,0.1c-0.5,0.2-1,0.5-1.3,0.9L3.7,8.5C3.2,9.1,3.3,10,3.9,10.5l0,0
|
||||
c0.3,0.2,0.6,0.3,0.9,0.3c0.4,0,0.8-0.2,1.1-0.5L8.6,7L11.4,8l-4.8,6.5l0,0c-0.1,0.1-0.1,0.3-0.2,0.4c-0.2,0.8,0.4,1.7,1.3,1.8
|
||||
l0.9,0.1l6.9,0.9l-1.7,2.7c-0.4,0.7-0.2,1.5,0.5,1.9h0c0.2,0.1,0.5,0.2,0.7,0.2c0.5,0,0.9-0.2,1.2-0.7l2-3.2
|
||||
c0.4-0.7,0.5-1.6,0.1-2.3C17.8,15.7,17.1,15.2,16.3,15.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M16,9.9c-2.7,0-4.8-2.2-4.8-4.8s2.2-4.8,4.8-4.8c2.7,0,4.8,2.2,4.8,4.8S18.6,9.9,16,9.9z M16,1.8c-1.8,0-3.3,1.5-3.3,3.3
|
||||
s1.5,3.3,3.3,3.3c1.8,0,3.3-1.5,3.3-3.3S17.8,1.8,16,1.8z"/>
|
||||
<path d="M19.6,32c-3.3,0-5.4-0.1-6-0.3c-2.6-1.1-2.6-2.5-2.4-3.1c0.5-2,4.1-3.1,8.7-2.6l0.3,0c0.4,0,0.8,0.3,0.8,0.8
|
||||
s-0.3,0.8-0.8,0.8h-0.3c-4.7-0.4-7,0.8-7.1,1.5c-0.1,0.3,0.4,0.9,1.5,1.4c0.8,0.3,7.5,0.2,10.1,0.2c0.7,0,1.3,0,1.7,0
|
||||
c0.6,0,1.3-0.5,1.3-1.3c0-1.6-2-3.1-3.6-4.2c-0.5-0.3-0.9-0.7-1.3-1c-0.2-0.2-0.4-0.4-0.7-0.6c-0.9-0.8-1.9-1.5-2-2.5
|
||||
c-0.1-0.5,0-1.1,0.2-1.8c0.2-0.6,0.3-1.2,0.3-2c0-0.4,0.3-0.7,0.6-0.7c0.4-0.1,0.7,0.1,0.8,0.5c0.9,2.3,1.4,2.7,2.7,3.9
|
||||
c0.6,0.5,1.3,1.1,2.4,2.1c0.5,0.5,1.2,0.5,1.6,0c0.2-0.2,0.3-0.5,0.3-0.8s-0.1-0.5-0.3-0.8l-2.9-2.8c-1.3-1.2-1.9-2.8-2.5-4
|
||||
c-0.7-1.7-1.1-2.5-2.1-2.5h-9c-0.9,0-1.3,0.7-2.1,2.5c-0.5,1.2-1.2,2.8-2.5,4l-2.9,2.8c-0.2,0.2-0.3,0.5-0.3,0.8
|
||||
c0,0.3,0.1,0.5,0.3,0.8c0.5,0.5,1.2,0.5,1.6,0c0.5-0.5,1-1,1.6-1.5c1.6-1.3,3.2-2.7,3.5-4.4c0.1-0.4,0.4-0.6,0.8-0.6
|
||||
c0.4,0,0.7,0.4,0.7,0.7c0,0.8,0.2,1.4,0.3,2c0.2,0.6,0.3,1.3,0.2,1.9c-0.2,0.9-1.2,1.7-2.1,2.5c-0.2,0.2-0.4,0.3-0.5,0.5
|
||||
c-0.4,0.3-0.8,0.7-1.3,1c-1.6,1.2-3.6,2.7-3.6,4.2c0,0.6,0.5,1.3,1.3,1.3h4c0.4,0,0.8,0.3,0.8,0.8S11.3,32,10.9,32h-4
|
||||
c-1.5,0-2.8-1.3-2.8-2.8c0-2.4,2.3-4.1,4.2-5.5c0.4-0.3,0.9-0.6,1.2-0.9c0.2-0.1,0.3-0.3,0.5-0.5c0.7-0.6,1.5-1.3,1.6-1.7
|
||||
c0.1-0.2,0-0.6-0.1-1c-0.8,1.1-1.9,2-2.9,2.9c-0.6,0.5-1.1,0.9-1.5,1.4c-1.1,1.1-2.7,1.1-3.7,0c-0.5-0.5-0.8-1.2-0.8-1.8
|
||||
s0.3-1.3,0.8-1.8l2.9-2.8c1.1-1,1.6-2.3,2.1-3.5c0.7-1.7,1.4-3.4,3.5-3.4h9c2,0,2.7,1.7,3.5,3.4c0.5,1.2,1.1,2.5,2.1,3.5l2.9,2.8
|
||||
c0.5,0.5,0.8,1.2,0.8,1.8s-0.3,1.3-0.8,1.8c-1.1,1.1-2.7,1-3.7,0c-1-0.9-1.7-1.5-2.3-2c-0.9-0.8-1.5-1.3-2.1-2.3
|
||||
c-0.1,0.4-0.2,0.8-0.2,1c0.1,0.5,0.9,1.1,1.5,1.6c0.2,0.2,0.5,0.4,0.7,0.6c0.3,0.3,0.8,0.6,1.2,0.9c1.9,1.4,4.2,3.1,4.2,5.5
|
||||
c0,1.5-1.3,2.8-2.8,2.8c-0.3,0-0.9,0-1.7,0C22.5,32,20.9,32,19.6,32z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.0 KiB |
18
webapp/assets/_new/icons/svgs/nature.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M31,20.4c-0.4-0.3-1-0.5-1.6-0.5l-0.2-0.4c-0.5-1.4-2.1-1.6-3.6-1.1c-0.2,0.1-0.6,0.3-1,0.6c-0.7-0.8-2-0.9-3.2-0.5
|
||||
c-0.5,0.2-1.8,0.9-2.5,1.3c-2.6-0.5-5.8-1.1-6.5-1.2c-2-0.2-3.3-0.2-4.9,0.3c-0.6,0.2-5.2,2.3-6.1,2.7L0.9,22l0.6,1.4l0.7-0.3
|
||||
c2.5-1.2,5.5-2.5,5.9-2.6c1.4-0.4,2.4-0.4,4.3-0.2c0.9,0.1,8.1,1.5,9.4,1.8c0.9,0.2,0.9,0.7,0.9,0.8c0,0.3-0.1,0.4-0.1,0.4
|
||||
s0,0-0.1,0h-8.6v1.5h2l0.5,0.3c0.1,0,1.4,0.9,2.4,1.3c1,0.4,2,0.4,2.7-0.2c2.6-1.7,6.7-4.4,7-4.6c0.6-0.4,1.4-0.4,1.6-0.2
|
||||
c0.1,0.1,0.2,0.2,0.2,0.3s-0.1,0.2-0.2,0.3c-2.8,2-10.3,7.5-11.2,8c-1.2,0.7-2.8,0.3-3.6,0.1c-1.2-0.4-7.1-2.5-7.6-2.7
|
||||
c-0.7-0.2-2.4-0.6-3.9,0C2.7,28,1,29,0.9,29l-0.7,0.3l0.7,1.3l0.7-0.3c0,0,1.7-0.9,2.8-1.4c0.9-0.4,2.2-0.2,2.6,0
|
||||
c0.5,0.2,6.4,2.3,7.7,2.7c0.5,0.1,1.3,0.3,2.2,0.3c0.8,0,1.7-0.2,2.5-0.3c1.1-0.6,10.9-7.8,11.3-8.1c0.5-0.4,0.8-0.9,0.8-1.5
|
||||
S31.5,20.8,31,20.4z M22,20c0,0,0.7-0.2,1.2-0.1c-0.4,0.3-0.8,0.5-1.1,0.7c0,0,0,0,0,0c-0.2-0.1-0.5-0.1-0.9-0.2
|
||||
C21.6,20.2,21.9,20.1,22,20z M19.3,25.2c-0.2-0.1-0.4-0.2-0.6-0.3h2.1c-0.1,0.1-0.2,0.2-0.3,0.2C20.3,25.3,19.9,25.5,19.3,25.2z
|
||||
M24.2,22.7c0-0.5-0.2-0.9-0.5-1.2c1.2-0.7,2.4-1.5,2.7-1.6c0,0,1.4-0.4,1.7,0.3l0,0.1c-0.2,0.1-0.3,0.1-0.5,0.2
|
||||
C27.5,20.5,25.8,21.6,24.2,22.7z"/>
|
||||
<path d="M15.9,16c3,0,4.5-1.3,5.3-2.3c1.3,1.4,1.5,3,1.5,3.1l0.1,0.7l1.6-0.1l-0.1-0.7c-0.1-0.1-0.3-2.5-2.5-4.4
|
||||
c0.4-1.6,0.1-3.5-1-4.9c-2.2-2.7-5.4-2.9-8.6-2.8c-2.9,0-4.6,0-6.4-0.9L4.9,3.2L4.7,4.2C4.3,6.3,5.2,9,7.1,11.5
|
||||
C9.4,14.4,12.7,16,15.9,16z M12.3,6.1c3-0.1,5.7,0,7.4,2.2c0.7,0.8,1,2,0.8,3c-2-1.1-4-1.6-5.6-2.1c-1.2-0.3-2.3-0.6-2.7-1
|
||||
l-0.6-0.5l-1,1.1l0.6,0.5c0.7,0.6,1.9,0.9,3.4,1.3c1.6,0.4,3.6,1,5.5,2.1c-0.7,1.2-2.1,1.8-4.1,1.8c-2.8,0-5.6-1.4-7.6-4
|
||||
C7,8.9,6.2,7,6.2,5.5C8,6.1,9.7,6.1,12.3,6.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
22
webapp/assets/_new/icons/svgs/networking.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M31.6,16c-0.6-6.4-1.9-6.4-2.4-6.4c-0.4,0-0.8,0.1-1.1,0.4c-0.8,0.8-0.8,2.5-0.8,4.2c0,0.5,0,1.2,0,1.7h-0.1
|
||||
C26,15.5,25.1,17,24.5,18c-0.1,0.2-0.2,0.4-0.3,0.5c-0.8,0.9-1.2,1.2-2.2,2c-0.4,0.4-1,0.6-1.5,0.9c-0.6,0.3-1.2,0.6-1.8,1.1
|
||||
c-2.3,1.9-1.8,4.1-1.4,6.1c0.2,0.8,0.3,1.5,0.3,2.3v0.8h1.5v-0.8c0-0.9-0.2-1.7-0.4-2.6c-0.4-2.1-0.6-3.4,0.9-4.6
|
||||
c0.4-0.4,1-0.6,1.5-0.9c0.6-0.3,1.2-0.6,1.8-1.1c1-0.8,1.5-1.2,2.4-2.2c0.1-0.1,0.3-0.4,0.4-0.7c0.2-0.3,0.7-1.2,1-1.4
|
||||
c0.3,0.2,0.4,1.3,0.3,1.7c-0.3,1-1.1,1.9-1.9,2.8c-0.7,0.8-1.4,1.6-1.8,2.5L23,25l1.3,0.7l0.3-0.7c0.4-0.7,1-1.4,1.6-2.2
|
||||
c0.9-1,1.8-2.1,2.2-3.3c0.2-0.5,0.2-1.4-0.1-2.2c0.4-0.5,0.4-1.4,0.4-3c0-0.8,0-2.5,0.3-3c0.3,0.5,0.7,1.8,1,4.9
|
||||
c0.2,2,0.2,4.8-1.2,7.1c-0.5,0.9-1.3,1.7-2,2.4c-1.3,1.4-2.7,2.9-3,5.2l-0.1,0.7l1.5,0.2l0.1-0.7c0.2-1.7,1.4-3,2.6-4.3
|
||||
c0.8-0.8,1.5-1.7,2.2-2.7C31.9,21.4,31.9,18.2,31.6,16z"/>
|
||||
<path d="M26.3,7.2c0-2.9-2.4-5.2-5.2-5.2c-2.4,0-3.9,1.4-4.5,2c-0.6-0.6-2.1-2-4.5-2C9.3,2,6.9,4.3,6.9,7.1c0,1,0.5,1.9,1,2.4
|
||||
c0.4,0.6,0.8,1,0.8,1l7.4,7.5l0.6,0.6l7.9-7.9C24.6,10.7,26.3,9.2,26.3,7.2z M16.6,16.7l-7-7c0,0-0.4-0.3-0.7-0.8
|
||||
C8.6,8.4,8.3,7.7,8.3,7.2c0-2.1,1.7-3.8,3.8-3.8c2,0,3.9,2,3.9,2L16.6,6l0.5-0.6c0,0,1.9-2,4-2s3.8,1.7,3.8,3.8
|
||||
c0,1.1-1.3,2.5-1.3,2.5L16.6,16.7z"/>
|
||||
<path d="M13.4,22.6c-0.6-0.4-1.2-0.8-1.8-1.1c-0.6-0.3-1.1-0.6-1.5-0.9c-1-0.8-1.4-1.2-2.2-2c-0.1-0.1-0.2-0.3-0.3-0.5
|
||||
c-0.6-1-1.5-2.5-2.7-2.1c-0.1,0-0.1,0.1-0.2,0.1c0-0.5,0-1.2,0-1.7c0-1.8,0-3.5-0.8-4.2c-0.3-0.3-0.7-0.4-1-0.4
|
||||
c-0.5,0-1.8,0-2.4,6.4c-0.3,2.2-0.2,5.4,1.4,8.1c0.6,1,1.4,1.9,2.2,2.7c1.3,1.3,2.4,2.6,2.7,4.3L6.9,32l1.5-0.2L8.1,31
|
||||
c-0.3-2.2-1.7-3.7-3.1-5.1c-0.7-0.8-1.5-1.6-2-2.5c-1.4-2.3-1.4-5.2-1.2-7.1c0.3-3.1,0.8-4.5,1-4.9c0.3,0.5,0.3,2.2,0.3,3
|
||||
c0,1.7,0,2.6,0.5,3.1c-0.2,0.8-0.2,1.7-0.1,2.1C4,20.9,4.9,22,5.8,23c0.6,0.7,1.3,1.4,1.6,2.2l0.3,0.7L9,25.2l-0.3-0.7
|
||||
c-0.5-0.9-1.2-1.7-1.8-2.5c-0.8-0.9-1.6-1.8-1.9-2.8c-0.1-0.4,0-1.5,0.3-1.7c0.3,0.2,0.8,1.1,1,1.4s0.3,0.6,0.5,0.7
|
||||
c0.8,0.9,1.4,1.4,2.4,2.2c0.6,0.4,1.2,0.8,1.8,1.1c0.6,0.3,1.1,0.6,1.5,0.9c1.5,1.3,1.3,2.6,0.9,4.6C13.1,29.3,13,30.1,13,31v0.8
|
||||
h1.5V31c0-0.7,0.2-1.5,0.3-2.3C15.2,26.8,15.7,24.5,13.4,22.6z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
8
webapp/assets/_new/icons/svgs/peace.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="32px" height="32px">
|
||||
<path d="M27.3,5c-1.5-1.5-3.1-2.6-5-3.4c-2-0.8-4-1.2-6.2-1.2c-2.1,0-4.2,0.4-6.2,1.2C8,2.4,6.3,3.5,4.9,5c-1.5,1.5-2.6,3.1-3.4,5
|
||||
c-0.8,2-1.2,4-1.2,6.2c0,2.1,0.4,4.2,1.2,6.2c0.8,1.9,1.9,3.6,3.4,5c1.5,1.5,3.1,2.6,5,3.4c2,0.8,4,1.2,6.2,1.2
|
||||
c2.1,0,4.2-0.4,6.2-1.2c1.9-0.8,3.6-1.9,5-3.4c1.5-1.5,2.6-3.1,3.4-5c0.8-2,1.2-4,1.2-6.2c0-2.1-0.4-4.2-1.2-6.2
|
||||
C29.8,8.1,28.7,6.4,27.3,5z M15.1,2.4v13.4L5.4,25c-2-2.4-3.2-5.5-3.2-8.8C2.2,8.9,7.9,2.9,15.1,2.4z M6.8,26.4l8.3-7.9V30
|
||||
C11.9,29.7,9,28.4,6.8,26.4z M17.1,30V18.5l8.3,7.9C23.1,28.4,20.2,29.7,17.1,30z M26.7,25l-9.7-9.2V2.4c7.2,0.5,12.8,6.5,12.8,13.8
|
||||
C29.9,19.5,28.7,22.6,26.7,25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 742 B |
12
webapp/assets/_new/icons/svgs/politics.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M19.3,15.4c0.3,0.2,0.9,0.5,1.1,0.8c0.5,0.6,0.6,1.2,0.6,1.3l0.8,3.1l1.4-0.4l-0.8-3c0-0.1-0.2-0.9-0.9-1.8
|
||||
c-0.5-0.6-1.5-1.1-1.6-1.2l-8.5-4.7l-0.7,1.3L19.3,15.4z"/>
|
||||
<path d="M7.4,19.1c0.4,2.2,2.5,4.1,3.4,4.8l0.9-1.2c-1.7-1.4-2.8-2.9-2.9-4.2l0-0.4l-2.3-1.3L11,7.8c0.1-0.2,0.1-0.4,0-0.6
|
||||
c-0.1-0.2-0.2-0.3-0.4-0.4L6.5,4.9L5.9,6.3l3.4,1.6L4.4,17.9l-3.4-1.6l-0.6,1.4l4.1,1.9c0.1,0,0.2,0.1,0.3,0.1
|
||||
c0.3,0,0.5-0.2,0.7-0.4l0.5-1L7.4,19.1z"/>
|
||||
<path d="M28.9,30.5l-2.8-10.1l-5.6,1.5l-1.1-4c-0.1-0.4-0.5-0.6-0.9-0.5c-0.4,0.1-0.6,0.5-0.5,0.9l2.1,7.6c0.2,0.7-0.2,1.4-1,1.6
|
||||
c-0.3,0.1-0.7,0-0.9-0.1c-0.3-0.2-0.5-0.4-0.6-0.8l-1.4-5c-0.1-0.3-0.4-0.5-0.7-0.6c-0.1,0-2.4,0-3.8-3.5c-0.2-0.4-0.6-0.6-1-0.4
|
||||
c-0.4,0.2-0.6,0.6-0.4,1c1.4,3.4,3.6,4.2,4.6,4.4l0.2,0.9l-6.8,1.9l1.2,4.4l1.4-0.4l-0.8-2.9l5.3-1.5l0.6,2.1
|
||||
c0.2,0.7,0.7,1.3,1.3,1.7c0.4,0.2,0.9,0.4,1.3,0.4c0.3,0,0.5,0,0.8-0.1c1.5-0.4,2.3-1.9,1.9-3.4L21,23.4l4.1-1.1l2.2,8.2h-20V32
|
||||
h24.5v-1.5H28.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
8
webapp/assets/_new/icons/svgs/psyche.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path class="st0" d="M30.8,12C30.5,7.4,27.9,3.5,24,1.8c-2-0.9-4.3-1.4-6.4-1.4v0.1c0,0,0,0,0,0V0.4c-2,0-3.9,0.4-5.7,1.2
|
||||
c-1.8,0.8-3.4,2-4.7,3.6c0,0,0,0,0,0c-0.1,0.1-2.6,3.3-2.5,8.5l-3.4,6.3C1,20.7,1,21.4,1.4,22c0.3,0.6,1,0.9,1.6,0.9h1.9v2.8
|
||||
c0,1.7,1.4,3.1,3.1,3.1h2.8v1.7c0,0.7,0,1.3,0,1.3l1.4,0v-4.3h-4c-1,0-1.9-0.8-1.9-1.9v-4H3.2c-0.5,0-0.8-0.5-0.6-1L6.2,14
|
||||
c-0.2-5,2.2-8,2.2-8c1.2-1.4,2.6-2.5,4.2-3.2c1.5-0.6,3.2-1,4.8-1c2,0,4,0.4,5.9,1.3c3.7,1.6,5.7,5.3,6,9.2c0.1,2.1-0.3,4.3-1.1,6.2
|
||||
c-0.4,1-0.9,1.9-1.5,2.8c-0.3,0.5-1.8,1.9-1.8,2.4c0,0,0,6,0,6.8l0,1.3c0.2,0,1.5,0,1.5,0s0-0.8,0-1.3c0,0,0-5.4,0-6.6
|
||||
c0.2-0.3,0.6-0.7,0.9-1c0.4-0.4,0.6-0.7,0.8-0.9c0.7-1,1.2-2,1.6-3.1C30.5,16.6,30.9,14.3,30.8,12z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 823 B |
5
webapp/assets/_new/icons/svgs/save.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>save</title>
|
||||
<path d="M5 5h17.406l0.313 0.281 4 4 0.281 0.313v17.406h-22v-22zM7 7v18h2v-9h14v9h2v-14.563l-3-3v5.563h-12v-6h-3zM12 7v4h8v-4h-2v2h-2v-2h-4zM11 18v7h10v-7h-10z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 327 B |
25
webapp/assets/_new/icons/svgs/science.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M28.4,16.1c1-1.7,1.9-3.3,2.5-4.9c1.5-4,1.3-7.3-0.6-9.2C27.5-0.8,22,0.1,16.1,3.7C10.3,0.1,4.7-0.8,2,1.9
|
||||
c-2.8,2.8-1.9,8.3,1.8,14.1c-3.3,5.3-4.4,10.4-2.5,13.4C1.5,29.7,1.7,30,2,30.2c1.1,1.1,2.6,1.6,4.4,1.6c1.4,0,3-0.3,4.8-1
|
||||
c1.6-0.6,3.2-1.4,4.9-2.5c2.6,1.6,5.1,2.7,7.4,3.2c0,0,0.1,0,0.1,0c0.3,0.1,0.7,0.1,1,0.2c0.1,0,0.1,0,0.2,0c0.4,0,0.7,0.1,1,0.1
|
||||
c0,0,0,0,0,0c0,0,0,0,0,0c0.3,0,0.5,0,0.8-0.1c0.2,0,0.4,0,0.5,0c0.3,0,0.6-0.1,0.9-0.2c0.1,0,0.2,0,0.3-0.1
|
||||
c0.3-0.1,0.5-0.2,0.8-0.4c0.1,0,0.2-0.1,0.3-0.1c0.3-0.2,0.6-0.4,0.9-0.7c1.9-1.9,2.1-5.2,0.6-9.2C30.3,19.4,29.5,17.7,28.4,16.1z
|
||||
M29.5,21.5c1.3,3.4,1.1,6.2-0.3,7.6c-0.2,0.2-0.4,0.3-0.6,0.5c-0.2,0.1-0.4,0.2-0.6,0.3c0,0-0.1,0-0.1,0.1
|
||||
c-0.3,0.1-0.5,0.2-0.8,0.2c0,0,0,0,0,0c-0.3,0.1-0.6,0.1-0.9,0.1c0,0,0,0,0,0c-0.3,0-0.6,0-0.8,0c-0.1,0-0.1,0-0.2,0
|
||||
c-0.2,0-0.5-0.1-0.7-0.1c-0.2,0-0.3,0-0.5-0.1c-0.1,0-0.3-0.1-0.4-0.1c-0.3-0.1-0.6-0.1-0.9-0.2c0,0-0.1,0-0.1,0
|
||||
c-1.5-0.5-3.2-1.3-4.9-2.3c1.9-1.3,3.7-2.8,5.4-4.6c1.7-1.7,3.3-3.6,4.6-5.5C28.3,18.8,29,20.2,29.5,21.5z M5.6,16.1
|
||||
c1.3-1.9,2.9-3.9,4.7-5.8c1.9-1.9,3.8-3.5,5.8-4.7c0,0,0,0,0.1,0c0.1,0.1,0.2,0.2,0.4,0.2c0.4,0.3,0.8,0.6,1.2,0.8c0,0,0,0,0.1,0.1
|
||||
c0.4,0.3,0.8,0.6,1.2,1c0.1,0.1,0.2,0.2,0.4,0.3c0.3,0.3,0.6,0.5,0.9,0.8c0.1,0.1,0.2,0.2,0.4,0.3c0.4,0.4,0.8,0.8,1.2,1.2
|
||||
c1.8,1.8,3.5,3.8,4.8,5.8c-1.3,2-2.9,3.9-4.8,5.8c-1.8,1.8-3.8,3.4-5.8,4.8c-1.9-1.3-3.9-2.9-5.8-4.8C8.5,20,6.9,18,5.6,16.1z
|
||||
M25.8,1.8c1.4,0,2.6,0.4,3.4,1.2c1.4,1.4,1.5,4.2,0.3,7.6c-0.5,1.3-1.2,2.7-2,4c-1.3-1.9-2.8-3.7-4.6-5.5
|
||||
c-1.8-1.8-3.6-3.3-5.4-4.6C20.6,2.8,23.5,1.8,25.8,1.8z M3,3c0.2-0.2,0.4-0.4,0.7-0.5C3.8,2.4,3.9,2.4,4,2.3
|
||||
c0.1-0.1,0.3-0.1,0.4-0.2C4.6,2.1,4.8,2,4.9,2c0.1,0,0.3-0.1,0.4-0.1c0.2,0,0.4,0,0.5,0c0.1,0,0.3,0,0.4,0c0.3,0,0.6,0,1,0.1
|
||||
c0.1,0,0.1,0,0.2,0C7.8,1.9,8.1,2,8.4,2c0.1,0,0.2,0,0.3,0.1c0.3,0.1,0.7,0.2,1,0.3c0,0,0.1,0,0.1,0c0.4,0.1,0.8,0.3,1.2,0.4
|
||||
c0.1,0,0.2,0.1,0.3,0.1c0.3,0.1,0.6,0.3,0.9,0.4c0.1,0,0.2,0.1,0.3,0.1c0.4,0.2,0.8,0.4,1.1,0.6c0,0,0.1,0,0.1,0
|
||||
c0.4,0.2,0.7,0.4,1.1,0.6c-1.8,1.3-3.7,2.8-5.4,4.6C7.5,11,6,12.8,4.7,14.7C1.8,9.7,0.9,5.1,3,3z M10.7,29.4
|
||||
c-3.4,1.3-6.2,1.1-7.6-0.3C0.9,27,1.8,22.4,4.7,17.5c1.3,1.8,2.8,3.7,4.6,5.4c0.4,0.4,0.9,0.8,1.3,1.2c0.1,0.1,0.2,0.2,0.3,0.3
|
||||
c0.4,0.3,0.8,0.7,1.1,1c0.1,0.1,0.1,0.1,0.2,0.2c0.8,0.7,1.7,1.3,2.5,1.9C13.4,28.3,12,28.9,10.7,29.4z"/>
|
||||
<path d="M19.4,19.3c1.8-1.8,1.8-4.8,0-6.6c-1.8-1.8-4.8-1.8-6.6,0c-1.8,1.8-1.8,4.8,0,6.6c0.9,0.9,2.1,1.4,3.3,1.4
|
||||
C17.3,20.7,18.5,20.3,19.4,19.3z M13.9,13.8c0.6-0.6,1.4-0.9,2.2-0.9s1.6,0.3,2.2,0.9c1.2,1.2,1.2,3.2,0,4.4s-3.2,1.2-4.4,0
|
||||
S12.7,15.1,13.9,13.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
13
webapp/assets/_new/icons/svgs/spirituality.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M15.9,18.6c-1.4,0-2.7-0.5-3.7-1.5c-1.4-1.4-1.9-3.3-1.4-5.2c0.5-1.8,1.9-3.2,3.7-3.7c1.9-0.5,3.8,0,5.2,1.4
|
||||
c1.3,1.3,1.9,3.3,1.4,5.2c-0.5,1.8-1.9,3.2-3.7,3.7c0,0,0,0,0,0C16.9,18.5,16.4,18.6,15.9,18.6z M17.2,17.7L17.2,17.7
|
||||
L17.2,17.7z M15.9,9.5c-0.3,0-0.7,0-1,0.1c-1.2,0.3-2.3,1.4-2.6,2.6c-0.4,1.4,0,2.7,1,3.7s2.4,1.3,3.7,1
|
||||
c1.2-0.3,2.3-1.4,2.6-2.6c0.4-1.4,0-2.7-1-3.7C17.9,9.9,16.9,9.5,15.9,9.5z"/>
|
||||
<path d="M27.8,32H4v-2.2c0-5.5,4.4-9.9,9.9-9.9h4c5.5,0,9.9,4.4,9.9,9.9V32z M5.5,30.5h20.8v-0.7c0-4.6-3.8-8.4-8.4-8.4h-4
|
||||
c-4.6,0-8.4,3.8-8.4,8.4V30.5z"/>
|
||||
<rect x="15.6" y="0.5" width="1.5" height="4.3"/>
|
||||
<rect x="23.6" y="7.2" transform="matrix(0.8679 -0.4968 0.4968 0.8679 -0.5339 13.83)" width="4.3" height="1.5"/>
|
||||
<rect x="5.9" y="5.9" transform="matrix(0.4969 -0.8678 0.8678 0.4969 -3.6304 9.7958)" width="1.5" height="4.3"/>
|
||||
<rect x="9.4" y="1.8" transform="matrix(0.8207 -0.5714 0.5714 0.8207 -0.4488 6.4807)" width="1.5" height="4.3"/>
|
||||
<rect x="20.1" y="3.2" transform="matrix(0.5712 -0.8208 0.8208 0.5712 6.3227 19.9037)" width="4.3" height="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -19,7 +19,6 @@ import { followUserMutation, unfollowUserMutation } from '~/graphql/User'
|
||||
|
||||
export default {
|
||||
name: 'HcFollowButton',
|
||||
|
||||
props: {
|
||||
followId: { type: String, default: null },
|
||||
isFollowed: { type: Boolean, default: false },
|
||||
136
webapp/components/Button/JoinLeaveButton.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<base-button
|
||||
class="track-button"
|
||||
:disabled="disabled"
|
||||
:loading="localLoading"
|
||||
:icon="icon"
|
||||
:filled="isMember && !hovered"
|
||||
:danger="isMember && hovered"
|
||||
@mouseenter.native="onHover"
|
||||
@mouseleave.native="hovered = false"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
{{ label }}
|
||||
</base-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import { joinGroupMutation, leaveGroupMutation } from '~/graphql/groups'
|
||||
|
||||
export default {
|
||||
name: 'JoinLeaveButton',
|
||||
props: {
|
||||
group: { type: Object, required: true },
|
||||
userId: { type: String, required: true },
|
||||
isMember: { type: Boolean, required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
loading: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localLoading: this.loading,
|
||||
hovered: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
if (this.isMember && this.hovered) {
|
||||
return 'close'
|
||||
} else {
|
||||
return this.isMember ? 'check' : 'plus'
|
||||
}
|
||||
},
|
||||
label() {
|
||||
if (this.isMember) {
|
||||
return this.$t('group.joinLeaveButton.iAmMember')
|
||||
} else {
|
||||
return this.$t('group.joinLeaveButton.join')
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isMember() {
|
||||
this.localLoading = false
|
||||
this.hovered = false
|
||||
},
|
||||
loading() {
|
||||
this.localLoading = this.loading
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
onHover() {
|
||||
if (!this.disabled && !this.localLoading) {
|
||||
this.hovered = true
|
||||
}
|
||||
},
|
||||
toggle() {
|
||||
if (this.isMember) {
|
||||
this.openLeaveModal()
|
||||
} else {
|
||||
this.joinLeave()
|
||||
}
|
||||
},
|
||||
openLeaveModal() {
|
||||
this.commitModalData(this.leaveModalData())
|
||||
},
|
||||
leaveModalData() {
|
||||
return {
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: 'group.leaveModal.title',
|
||||
messageIdent: 'group.leaveModal.message',
|
||||
messageParams: {
|
||||
name: this.group.name,
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: 'sign-out',
|
||||
textIdent: 'group.leaveModal.confirmButton',
|
||||
callback: this.joinLeave,
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
async joinLeave() {
|
||||
const join = !this.isMember
|
||||
const mutation = join ? joinGroupMutation : leaveGroupMutation
|
||||
|
||||
this.hovered = false
|
||||
this.$emit('prepare', join)
|
||||
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation,
|
||||
variables: { groupId: this.group.id, userId: this.userId },
|
||||
})
|
||||
const joinedLeftGroupResult = join ? data.JoinGroup : data.LeaveGroup
|
||||
this.$emit('update', joinedLeftGroupResult)
|
||||
} catch (error) {
|
||||
this.$toast.error(error.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.track-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -15,8 +15,19 @@ describe('CategoriesFilter.vue', () => {
|
||||
'posts/filteredCategoryIds': jest.fn(() => []),
|
||||
}
|
||||
|
||||
const apolloMutationMock = jest.fn().mockResolvedValue({
|
||||
data: { saveCategorySettings: true },
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((string) => string),
|
||||
$apollo: {
|
||||
mutate: apolloMutationMock,
|
||||
},
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
@ -76,5 +87,14 @@ describe('CategoriesFilter.vue', () => {
|
||||
expect(mutations['posts/RESET_CATEGORIES']).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('save categories', () => {
|
||||
it('calls the API', async () => {
|
||||
wrapper = await Wrapper()
|
||||
const saveButton = wrapper.findAll('.categories-filter .sidebar .base-button').at(1)
|
||||
saveButton.trigger('click')
|
||||
expect(apolloMutationMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
icon="check"
|
||||
@click="resetCategories"
|
||||
/>
|
||||
<hr />
|
||||
<labeled-button filled :label="$t('actions.save')" icon="save" @click="saveCategories" />
|
||||
</template>
|
||||
<template #filter-list>
|
||||
<li v-for="category in categories" :key="category.id" class="item">
|
||||
@ -24,6 +26,7 @@
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import CategoryQuery from '~/graphql/CategoryQuery.js'
|
||||
import SaveCategories from '~/graphql/SaveCategories.js'
|
||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||
import LabeledButton from '~/components/_new/generic/LabeledButton/LabeledButton'
|
||||
|
||||
@ -47,6 +50,19 @@ export default {
|
||||
resetCategories: 'posts/RESET_CATEGORIES',
|
||||
toggleCategory: 'posts/TOGGLE_CATEGORY',
|
||||
}),
|
||||
saveCategories() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: SaveCategories(),
|
||||
variables: { activeCategories: this.filteredCategoryIds },
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('filter-menu.save.success'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$toast.error(this.$t('filter-menu.save.error'))
|
||||
})
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
Category: {
|
||||
|
||||
@ -12,6 +12,8 @@ config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||
config.stubs['locale-switch'] = '<span><slot /></span>'
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
|
||||
const authUserMock = jest.fn().mockReturnValue({ activeCategories: [] })
|
||||
|
||||
describe('LoginForm', () => {
|
||||
let mocks
|
||||
let propsData
|
||||
@ -26,10 +28,15 @@ describe('LoginForm', () => {
|
||||
storeMocks = {
|
||||
getters: {
|
||||
'auth/pending': () => false,
|
||||
'auth/user': authUserMock,
|
||||
},
|
||||
actions: {
|
||||
'auth/login': jest.fn(),
|
||||
},
|
||||
mutations: {
|
||||
'posts/TOGGLE_CATEGORY': jest.fn(),
|
||||
'posts/RESET_CATEGORIES': jest.fn(),
|
||||
},
|
||||
}
|
||||
const store = new Vuex.Store(storeMocks)
|
||||
mocks = {
|
||||
@ -43,20 +50,46 @@ describe('LoginForm', () => {
|
||||
}
|
||||
|
||||
describe('fill in email and password and submit', () => {
|
||||
const fillIn = (wrapper, opts = {}) => {
|
||||
const fillIn = async (wrapper, opts = {}) => {
|
||||
const { email = 'email@example.org', password = '1234' } = opts
|
||||
wrapper.find('input[name="email"]').setValue(email)
|
||||
wrapper.find('input[name="password"]').setValue(password)
|
||||
wrapper.find('form').trigger('submit')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
}
|
||||
|
||||
it('dispatches login with form data', () => {
|
||||
fillIn(Wrapper())
|
||||
it('dispatches login with form data', async () => {
|
||||
await fillIn(Wrapper())
|
||||
expect(storeMocks.actions['auth/login']).toHaveBeenCalledWith(expect.any(Object), {
|
||||
email: 'email@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
describe('setting saved categories', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('no categories saved', () => {
|
||||
it('resets the categories', async () => {
|
||||
await fillIn(Wrapper())
|
||||
expect(storeMocks.mutations['posts/RESET_CATEGORIES']).toBeCalled()
|
||||
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('categories saved', () => {
|
||||
it('sets the categories', async () => {
|
||||
authUserMock.mockReturnValue({ activeCategories: ['cat1', 'cat9', 'cat12'] })
|
||||
await fillIn(Wrapper())
|
||||
expect(storeMocks.mutations['posts/RESET_CATEGORIES']).toBeCalled()
|
||||
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledTimes(3)
|
||||
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledWith({}, 'cat1')
|
||||
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledWith({}, 'cat9')
|
||||
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledWith({}, 'cat12')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Visibility of password', () => {
|
||||
|
||||
@ -58,6 +58,7 @@ import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParams
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
import Logo from '~/components/Logo/Logo'
|
||||
import ShowPassword from '../ShowPassword/ShowPassword.vue'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -84,12 +85,27 @@ export default {
|
||||
iconName() {
|
||||
return this.showPassword ? 'eye-slash' : 'eye'
|
||||
},
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
toggleCategory: 'posts/TOGGLE_CATEGORY',
|
||||
resetCategories: 'posts/RESET_CATEGORIES',
|
||||
}),
|
||||
async onSubmit() {
|
||||
const { email, password } = this.form
|
||||
try {
|
||||
await this.$store.dispatch('auth/login', { email, password })
|
||||
if (this.currentUser && this.currentUser.activeCategories) {
|
||||
this.resetCategories()
|
||||
if (this.currentUser.activeCategories.length > 0) {
|
||||
this.currentUser.activeCategories.forEach((categoryId) => {
|
||||
this.toggleCategory(categoryId)
|
||||
})
|
||||
}
|
||||
}
|
||||
this.$toast.success(this.$t('login.success'))
|
||||
this.$emit('success')
|
||||
} catch (err) {
|
||||
|
||||
41
webapp/components/features/ProfileList/FollowList.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<profile-list
|
||||
:uniqueName="`${type}Filter`"
|
||||
:title="$filters.truncate(userName, 15) + ' ' + $t(`profile.network.${type}`)"
|
||||
:titleNobody="$filters.truncate(userName, 15) + ' ' + $t(`profile.network.${type}Nobody`)"
|
||||
:allProfilesCount="allConnectionsCount"
|
||||
:profiles="connections"
|
||||
:loading="loading"
|
||||
@fetchAllProfiles="$emit('fetchAllConnections', type)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProfileList, { profileListVisibleCount } from '~/components/features/ProfileList/ProfileList'
|
||||
|
||||
export const followListVisibleCount = profileListVisibleCount
|
||||
|
||||
export default {
|
||||
name: 'FollowerList',
|
||||
components: {
|
||||
ProfileList,
|
||||
},
|
||||
props: {
|
||||
user: { type: Object, default: null },
|
||||
type: { type: String, default: 'following' },
|
||||
loading: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
userName() {
|
||||
const { name } = this.user || {}
|
||||
return name || this.$t('profile.userAnonym')
|
||||
},
|
||||
allConnectionsCount() {
|
||||
return this.user[`${this.type}Count`]
|
||||
},
|
||||
connections() {
|
||||
return this.user[this.type]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<base-card class="follow-list">
|
||||
<template v-if="connections && connections.length">
|
||||
<base-card class="profile-list">
|
||||
<template v-if="profiles.length">
|
||||
<h5 class="title spacer-x-small">
|
||||
{{ userName | truncate(15) }} {{ $t(`profile.network.${type}`) }}
|
||||
{{ title }}
|
||||
</h5>
|
||||
<ul :class="connectionsClass">
|
||||
<ul :class="profilesClass">
|
||||
<li
|
||||
v-for="connection in filteredConnections"
|
||||
:key="connection.id"
|
||||
@ -13,30 +13,30 @@
|
||||
<user-teaser :user="connection" />
|
||||
</li>
|
||||
</ul>
|
||||
<base-button
|
||||
v-if="hasMore"
|
||||
:loading="loading"
|
||||
class="spacer-x-small"
|
||||
size="small"
|
||||
@click="$emit('fetchAllConnections', type)"
|
||||
>
|
||||
{{
|
||||
$t('profile.network.andMore', {
|
||||
number: allConnectionsCount - connections.length,
|
||||
})
|
||||
}}
|
||||
</base-button>
|
||||
<ds-input
|
||||
v-if="!hasMore"
|
||||
:name="`${type}Filter`"
|
||||
v-if="isMoreAsVisible"
|
||||
:name="uniqueName"
|
||||
:placeholder="filter"
|
||||
class="spacer-x-small"
|
||||
icon="filter"
|
||||
size="small"
|
||||
@input.native="setFilter"
|
||||
/>
|
||||
<base-button
|
||||
v-if="hasMore"
|
||||
:loading="loading"
|
||||
class="spacer-x-small"
|
||||
size="small"
|
||||
@click="$emit('fetchAllProfiles')"
|
||||
>
|
||||
{{
|
||||
$t('profile.network.andMore', {
|
||||
number: allProfilesCount - profiles.length,
|
||||
})
|
||||
}}
|
||||
</base-button>
|
||||
</template>
|
||||
<p v-else class="nobody-message">{{ userName }} {{ $t(`profile.network.${type}Nobody`) }}</p>
|
||||
<p v-else-if="titleNobody" class="nobody-message">{{ titleNobody }}</p>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
@ -44,41 +44,40 @@
|
||||
import { escape } from 'xregexp/xregexp-all.js'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
|
||||
export const profileListVisibleCount = 7
|
||||
|
||||
export default {
|
||||
name: 'FollowerList',
|
||||
name: 'ProfileList',
|
||||
components: {
|
||||
UserTeaser,
|
||||
},
|
||||
props: {
|
||||
user: { type: Object, default: null },
|
||||
type: { type: String, default: 'following' },
|
||||
uniqueName: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
titleNobody: { type: String, default: null },
|
||||
allProfilesCount: { type: Number, required: true },
|
||||
profiles: { type: Array, required: true },
|
||||
loading: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
profileListVisibleCount,
|
||||
filter: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userName() {
|
||||
const { name } = this.user || {}
|
||||
return name || this.$t('profile.userAnonym')
|
||||
},
|
||||
allConnectionsCount() {
|
||||
return this.user[`${this.type}Count`]
|
||||
},
|
||||
connections() {
|
||||
return this.user[this.type]
|
||||
},
|
||||
hasMore() {
|
||||
return this.allConnectionsCount > this.connections.length
|
||||
return this.allProfilesCount > this.profiles.length
|
||||
},
|
||||
connectionsClass() {
|
||||
return `connections${this.hasMore ? '' : ' --overflow'}`
|
||||
isMoreAsVisible() {
|
||||
return this.profiles.length > this.profileListVisibleCount
|
||||
},
|
||||
profilesClass() {
|
||||
return `profiles${this.isMoreAsVisible ? ' --overflow' : ''}`
|
||||
},
|
||||
filteredConnections() {
|
||||
if (!this.filter) {
|
||||
return this.connections
|
||||
return this.profiles
|
||||
}
|
||||
|
||||
// @example
|
||||
@ -89,7 +88,7 @@ export default {
|
||||
'i',
|
||||
)
|
||||
|
||||
const fuzzyScores = this.connections
|
||||
const fuzzyScores = this.profiles
|
||||
.map((user) => {
|
||||
const match = user.name.match(fuzzyExpression)
|
||||
|
||||
@ -122,7 +121,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.follow-list {
|
||||
.profile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@ -133,7 +132,7 @@ export default {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
.connections {
|
||||
.profiles {
|
||||
height: $size-height-connections;
|
||||
padding: $space-none;
|
||||
list-style-type: none;
|
||||
9
webapp/graphql/SaveCategories.js
Normal file
@ -0,0 +1,9 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default () => {
|
||||
return gql`
|
||||
mutation ($activeCategories: [String]) {
|
||||
saveCategorySettings(activeCategories: $activeCategories)
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -285,6 +285,7 @@ export const currentUserQuery = gql`
|
||||
id
|
||||
url
|
||||
}
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -131,36 +131,8 @@ export const changeGroupMemberRoleMutation = gql`
|
||||
// ------ queries
|
||||
|
||||
export const groupQuery = gql`
|
||||
query (
|
||||
$isMember: Boolean
|
||||
$id: ID
|
||||
$name: String
|
||||
$slug: String
|
||||
$createdAt: String
|
||||
$updatedAt: String
|
||||
$about: String
|
||||
$description: String
|
||||
$locationName: String
|
||||
$first: Int
|
||||
$offset: Int
|
||||
$orderBy: [_GroupOrdering]
|
||||
$filter: _GroupFilter
|
||||
) {
|
||||
Group(
|
||||
isMember: $isMember
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
createdAt: $createdAt
|
||||
updatedAt: $updatedAt
|
||||
about: $about
|
||||
description: $description
|
||||
locationName: $locationName
|
||||
first: $first
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
filter: $filter
|
||||
) {
|
||||
query ($isMember: Boolean, $id: ID, $slug: String) {
|
||||
Group(isMember: $isMember, id: $id, slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
@ -189,8 +161,8 @@ export const groupQuery = gql`
|
||||
`
|
||||
|
||||
export const groupMembersQuery = gql`
|
||||
query ($id: ID!, $first: Int, $offset: Int, $orderBy: [_UserOrdering], $filter: _UserFilter) {
|
||||
GroupMembers(id: $id, first: $first, offset: $offset, orderBy: $orderBy, filter: $filter) {
|
||||
query ($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
|
||||
@ -364,7 +364,11 @@
|
||||
"label": "Älteste zuerst"
|
||||
}
|
||||
},
|
||||
"order-by": "Sortieren nach ..."
|
||||
"order-by": "Sortieren nach ...",
|
||||
"save": {
|
||||
"error": "Themen konnten nicht gespeichert werden!",
|
||||
"success": "Themen gespeichert!"
|
||||
}
|
||||
},
|
||||
"followButton": {
|
||||
"follow": "Folgen",
|
||||
@ -375,6 +379,16 @@
|
||||
"change-member-role": "Die Rolle wurde auf ({role}) geändert!",
|
||||
"follow": "Folge",
|
||||
"general": "Allgemein",
|
||||
"actionRadii": {
|
||||
"continental": "Kontinentale Gruppe",
|
||||
"global": "Globale Gruppe",
|
||||
"interplanetary": "Interplanetare Gruppe",
|
||||
"national": "Nationale Gruppe",
|
||||
"regional": "Regionale Gruppe"
|
||||
},
|
||||
"actionRadius": "Aktionsradius",
|
||||
"foundation": "Gründung",
|
||||
"goal": "Ziel der Gruppe",
|
||||
"group-created": "Die Gruppe wurde angelegt!",
|
||||
"group-name": "Gruppenname",
|
||||
"group-updated": "Die Gruppendaten wurden geändert!",
|
||||
@ -393,6 +407,33 @@
|
||||
"unfollowing": "Nicht mehr folgen",
|
||||
"update": "Änderung speichern",
|
||||
"visibility": "Sichtbarkeit"
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": "Bin Mitglied",
|
||||
"join": "Beitreten"
|
||||
},
|
||||
"leaveModal": {
|
||||
"confirmButton": "Verlassen",
|
||||
"message": "Eine Gruppe zu verlassen ist möglicherweise nicht rückgängig zu machen!<br>Gruppe <b>„{name}“</b> verlassen!",
|
||||
"title": "Möchtest du wirklich die Gruppe verlassen?"
|
||||
},
|
||||
"membersCount": "Mitglieder",
|
||||
"membersListTitle": "Gruppenmitglieder",
|
||||
"newGroup": "Erstelle eine neue Gruppe",
|
||||
"role": "Deine Rolle in der Gruppe",
|
||||
"roles": {
|
||||
"admin": "Administrator",
|
||||
"owner": "Inhaber",
|
||||
"pending": "Ausstehendes Mitglied",
|
||||
"usual": "Einfaches Mitglied"
|
||||
},
|
||||
"save": "Neue Gruppe anlegen",
|
||||
"type": "Gruppentyp",
|
||||
"types": {
|
||||
"closed": "Geschlossene Gruppe",
|
||||
"hidden": "Versteckte Gruppe",
|
||||
"public": "Öffentliche Gruppe"
|
||||
},
|
||||
"update": "Änderung speichern"
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"clearSearch": "Suche löschen",
|
||||
@ -551,8 +592,6 @@
|
||||
"follow": "Folgen",
|
||||
"followers": "Folgen",
|
||||
"following": "Folge Ich",
|
||||
"groupGoal": "Ziel:",
|
||||
"groupSince": "Gründung",
|
||||
"invites": {
|
||||
"description": "Zur Einladung die E-Mail-Adresse hier eintragen.",
|
||||
"emailPlaceholder": "E-Mail-Adresse für die Einladung",
|
||||
|
||||
@ -364,7 +364,11 @@
|
||||
"label": "Oldest first"
|
||||
}
|
||||
},
|
||||
"order-by": "Order by ..."
|
||||
"order-by": "Order by ...",
|
||||
"save": {
|
||||
"error": "Failed saving topic settings!",
|
||||
"success": "Topics saved!"
|
||||
}
|
||||
},
|
||||
"followButton": {
|
||||
"follow": "Follow",
|
||||
@ -375,6 +379,16 @@
|
||||
"change-member-role": "The role has been changed to ({role})!",
|
||||
"follow": "Follow",
|
||||
"general": "General",
|
||||
"actionRadii": {
|
||||
"continental": "Continental Group",
|
||||
"global": "Global Group",
|
||||
"interplanetary": "Interplanetary Group",
|
||||
"national": "National Group",
|
||||
"regional": "Regional Group"
|
||||
},
|
||||
"actionRadius": "Action radius",
|
||||
"foundation": "Foundation",
|
||||
"goal": "Goal of group",
|
||||
"group-created": "The group was created!",
|
||||
"group-name": "Group name",
|
||||
"group-updated": "The group data has been changed.",
|
||||
@ -393,6 +407,33 @@
|
||||
"unfollowing": "unfollowing",
|
||||
"update": "Save change",
|
||||
"visibility": "Visibility"
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": "I'm a member",
|
||||
"join": "Join"
|
||||
},
|
||||
"leaveModal": {
|
||||
"confirmButton": "Leave",
|
||||
"message": "Leaving a group may be irreversible!<br>Leave group <b>“{name}”</b>!",
|
||||
"title": "Do you really want to leave the group?"
|
||||
},
|
||||
"membersCount": "Members",
|
||||
"membersListTitle": "Group Members",
|
||||
"newGroup": "Create a new Group",
|
||||
"role": "Your role in the group",
|
||||
"roles": {
|
||||
"admin": "Administrator",
|
||||
"owner": "Owner",
|
||||
"pending": "Pending Member",
|
||||
"usual": "Simple Member"
|
||||
},
|
||||
"save": "Create new group",
|
||||
"type": "Group type",
|
||||
"types": {
|
||||
"closed": "Closed Group",
|
||||
"hidden": "Hidden Group",
|
||||
"public": "Public Group"
|
||||
},
|
||||
"update": "Save change"
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"clearSearch": "Clear search",
|
||||
@ -551,8 +592,6 @@
|
||||
"follow": "Follow",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"groupGoal": "Goal:",
|
||||
"groupSince": "Foundation",
|
||||
"invites": {
|
||||
"description": "Enter their e-mail address for invitation.",
|
||||
"emailPlaceholder": "E-mail to invite",
|
||||
|
||||
@ -297,6 +297,16 @@
|
||||
"follow": "Seguir",
|
||||
"following": "Siguiendo"
|
||||
},
|
||||
"group": {
|
||||
"foundation": null,
|
||||
"goal": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null
|
||||
},
|
||||
"membersCount": null,
|
||||
"membersListTitle": null
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"clearSearch": "Borrar búsqueda",
|
||||
"hashtag-search": "Buscando a #{hashtag}",
|
||||
|
||||
@ -286,6 +286,16 @@
|
||||
"follow": "Suivre",
|
||||
"following": "Je suis les"
|
||||
},
|
||||
"group": {
|
||||
"foundation": null,
|
||||
"goal": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null
|
||||
},
|
||||
"membersCount": null,
|
||||
"membersListTitle": null
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"clearSearch": "Réinitialiser la recherche",
|
||||
"hashtag-search": "Recherche de #{hashtag}",
|
||||
|
||||
@ -294,6 +294,16 @@
|
||||
"follow": null,
|
||||
"following": null
|
||||
},
|
||||
"group": {
|
||||
"foundation": null,
|
||||
"goal": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null
|
||||
},
|
||||
"membersCount": null,
|
||||
"membersListTitle": null
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"clearSearch": null,
|
||||
"hashtag-search": null,
|
||||
|
||||
@ -82,6 +82,16 @@
|
||||
"follow": "Volgen",
|
||||
"following": "Volgt"
|
||||
},
|
||||
"group": {
|
||||
"foundation": null,
|
||||
"goal": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null
|
||||
},
|
||||
"membersCount": null,
|
||||
"membersListTitle": null
|
||||
},
|
||||
"login": {
|
||||
"email": "Uw E-mail",
|
||||
"hello": "Hallo",
|
||||
|
||||
@ -166,6 +166,16 @@
|
||||
"follow": "naśladować",
|
||||
"following": "w skutek"
|
||||
},
|
||||
"group": {
|
||||
"foundation": null,
|
||||
"goal": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null
|
||||
},
|
||||
"membersCount": null,
|
||||
"membersListTitle": null
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"title": "Twoja bańka filtrująca"
|
||||
},
|
||||
|
||||
@ -332,6 +332,16 @@
|
||||
"follow": "Seguir",
|
||||
"following": "Seguindo"
|
||||
},
|
||||
"group": {
|
||||
"foundation": null,
|
||||
"goal": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null
|
||||
},
|
||||
"membersCount": null,
|
||||
"membersListTitle": null
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"clearSearch": "Limpar pesquisa",
|
||||
"hashtag-search": "Procurando por #{hashtag}",
|
||||
|
||||
@ -311,6 +311,16 @@
|
||||
"follow": "Подписаться",
|
||||
"following": "Вы подписаны"
|
||||
},
|
||||
"group": {
|
||||
"foundation": null,
|
||||
"goal": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null
|
||||
},
|
||||
"membersCount": null,
|
||||
"membersListTitle": null
|
||||
},
|
||||
"hashtags-filter": {
|
||||
"clearSearch": "Очистить поиск",
|
||||
"hashtag-search": "Поиск по #{hashtag}",
|
||||
|
||||
@ -10,22 +10,24 @@ export default function (options = {}) {
|
||||
} = context
|
||||
const idOrSlug = id || slug
|
||||
|
||||
const variables = { idOrSlug }
|
||||
const client = apolloProvider.defaultClient
|
||||
if (idOrSlug) {
|
||||
const variables = { idOrSlug }
|
||||
const client = apolloProvider.defaultClient
|
||||
|
||||
let response
|
||||
let resource
|
||||
response = await client.query({ query: queryId, variables })
|
||||
resource = response.data[Object.keys(response.data)[0]][0]
|
||||
if (resource && resource.slug === slug) return // all good
|
||||
if (resource && resource.slug !== slug) {
|
||||
return redirect(`/${path}/${resource.id}/${resource.slug}`)
|
||||
let response
|
||||
let resource
|
||||
response = await client.query({ query: queryId, variables })
|
||||
resource = response.data[Object.keys(response.data)[0]][0]
|
||||
if (resource && resource.slug === slug) return // all good
|
||||
if (resource && resource.slug !== slug) {
|
||||
return redirect(`/${path}/${resource.id}/${resource.slug}`)
|
||||
}
|
||||
|
||||
response = await client.query({ query: querySlug, variables })
|
||||
resource = response.data[Object.keys(response.data)[0]][0]
|
||||
if (resource) return redirect(`/${path}/${resource.id}/${resource.slug}`)
|
||||
}
|
||||
|
||||
response = await client.query({ query: querySlug, variables })
|
||||
resource = response.data[Object.keys(response.data)[0]][0]
|
||||
if (resource) return redirect(`/${path}/${resource.id}/${resource.slug}`)
|
||||
|
||||
return error({ statusCode: 404, key: message })
|
||||
},
|
||||
}
|
||||
|
||||
@ -42,50 +42,98 @@
|
||||
{{ user.location.name }}
|
||||
</ds-text> -->
|
||||
<ds-text align="center" color="soft" size="small">
|
||||
{{ $t('profile.groupSince') }} {{ group.createdAt | date('MMMM yyyy') }}
|
||||
{{ $t('group.foundation') }} {{ group.createdAt | date('MMMM yyyy') }}
|
||||
</ds-text>
|
||||
</ds-space>
|
||||
<ds-flex>
|
||||
<ds-flex-item>
|
||||
<!-- <client-only>
|
||||
<client-only>
|
||||
<ds-number :label="$t('group.membersCount')">
|
||||
<count-to
|
||||
slot="count"
|
||||
:start-val="membersCountStartValue"
|
||||
:end-val="groupMembers.length"
|
||||
/>
|
||||
</ds-number>
|
||||
</client-only>
|
||||
</ds-flex-item>
|
||||
<!-- <ds-flex-item>
|
||||
<client-only>
|
||||
<ds-number :label="$t('profile.followers')">
|
||||
<hc-count-to
|
||||
<count-to
|
||||
slot="count"
|
||||
:start-val="followedByCountStartValue"
|
||||
:end-val="user.followedByCount"
|
||||
/>
|
||||
</ds-number>
|
||||
</client-only> -->
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<!-- <client-only>
|
||||
</client-only>
|
||||
</ds-flex-item> -->
|
||||
<!-- <ds-flex-item>
|
||||
<client-only>
|
||||
<ds-number :label="$t('profile.following')">
|
||||
<hc-count-to slot="count" :end-val="user.followingCount" />
|
||||
<count-to slot="count" :end-val="user.followingCount" />
|
||||
</ds-number>
|
||||
</client-only> -->
|
||||
</ds-flex-item>
|
||||
</client-only>
|
||||
</ds-flex-item> -->
|
||||
</ds-flex>
|
||||
<div v-if="!isGroupMember" class="action-buttons">
|
||||
<div class="action-buttons">
|
||||
<!-- <base-button v-if="user.isBlocked" @click="unblockUser(user)">
|
||||
{{ $t('settings.blocked-users.unblock') }}
|
||||
</base-button>
|
||||
<base-button v-if="user.isMuted" @click="unmuteUser(user)">
|
||||
{{ $t('settings.muted-users.unmute') }}
|
||||
</base-button>
|
||||
<hc-follow-button
|
||||
<follow-button
|
||||
v-if="!user.isMuted && !user.isBlocked"
|
||||
:follow-id="user.id"
|
||||
:is-followed="user.followedByCurrentUser"
|
||||
@optimistic="optimisticFollow"
|
||||
@update="updateFollow"
|
||||
/> -->
|
||||
<join-leave-button
|
||||
:group="group || {}"
|
||||
:userId="currentUser.id"
|
||||
:isMember="isGroupMember"
|
||||
:disabled="isGroupOwner"
|
||||
:loading="$apollo.loading"
|
||||
@prepare="prepareJoinLeave"
|
||||
@update="updateJoinLeave"
|
||||
/>
|
||||
<!-- implement:
|
||||
v-if="!user.isMuted && !user.isBlocked" -->
|
||||
</div>
|
||||
<hr />
|
||||
<ds-space margin-top="small" margin-bottom="small">
|
||||
<template v-if="isGroupMember">
|
||||
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
|
||||
{{ $t('group.role') }}
|
||||
</ds-text>
|
||||
<div class="chip" align="center">
|
||||
<ds-chip color="primary">{{ $t('group.roles.' + group.myRole) }}</ds-chip>
|
||||
</div>
|
||||
</template>
|
||||
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
|
||||
{{ $t('group.type') }}
|
||||
</ds-text>
|
||||
<div class="chip" align="center">
|
||||
<ds-chip color="primary">{{ $t('group.types.' + group.groupType) }}</ds-chip>
|
||||
</div>
|
||||
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
|
||||
{{ $t('group.actionRadius') }}
|
||||
</ds-text>
|
||||
<div class="chip" align="center">
|
||||
<ds-chip color="primary">{{ $t('group.actionRadii.' + group.actionRadius) }}</ds-chip>
|
||||
</div>
|
||||
</ds-space>
|
||||
<template v-if="group.about">
|
||||
<hr />
|
||||
<ds-space margin-top="small" margin-bottom="small">
|
||||
<ds-text color="soft" size="small" class="hyphenate-text">
|
||||
{{ $t('profile.groupGoal') }} {{ group.about }}
|
||||
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
|
||||
{{ $t('group.goal') }}
|
||||
</ds-text>
|
||||
<div class="chip" align="center">
|
||||
<ds-chip>{{ group.about }}</ds-chip>
|
||||
</div>
|
||||
</ds-space>
|
||||
</template>
|
||||
</base-card>
|
||||
@ -93,7 +141,17 @@
|
||||
<ds-heading tag="h3" soft style="text-align: center; margin-bottom: 10px">
|
||||
{{ $t('profile.network.title') }}
|
||||
</ds-heading>
|
||||
<!-- <follow-list
|
||||
<!-- Group members list -->
|
||||
<profile-list
|
||||
:uniqueName="`groupMembersFilter`"
|
||||
:title="$t('group.membersListTitle')"
|
||||
:allProfilesCount="groupMembers.length"
|
||||
:profiles="groupMembers"
|
||||
:loading="$apollo.loading"
|
||||
@fetchAllProfiles="fetchAllMembers"
|
||||
/>
|
||||
<!-- <ds-space />
|
||||
<follow-list
|
||||
:loading="$apollo.loading"
|
||||
:user="user"
|
||||
type="followedBy"
|
||||
@ -159,7 +217,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<ds-grid-item column-span="fullWidth">
|
||||
<hc-empty margin="xx-large" icon="file" />
|
||||
<empty margin="xx-large" icon="file" />
|
||||
</ds-grid-item>
|
||||
</template>
|
||||
</masonry-grid>
|
||||
@ -173,26 +231,26 @@
|
||||
|
||||
<script>
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
// import HcFollowButton from '~/components/FollowButton.vue'
|
||||
// import HcCountTo from '~/components/CountTo.vue'
|
||||
// import HcBadges from '~/components/Badges.vue'
|
||||
// import FollowList from '~/components/features/FollowList/FollowList'
|
||||
import HcEmpty from '~/components/Empty/Empty'
|
||||
// import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import AvatarUploader from '~/components/Uploader/AvatarUploader'
|
||||
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
|
||||
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
// import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
|
||||
// import { profilePagePosts } from '~/graphql/PostQuery'
|
||||
import { groupQuery } from '~/graphql/groups'
|
||||
import { updateGroupMutation } from '~/graphql/groups.js'
|
||||
import { updateGroupMutation, groupQuery, groupMembersQuery } from '~/graphql/groups'
|
||||
// import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
||||
// import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
|
||||
// import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
import AvatarUploader from '~/components/Uploader/AvatarUploader'
|
||||
// import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import CountTo from '~/components/CountTo.vue'
|
||||
import Empty from '~/components/Empty/Empty'
|
||||
// import FollowButton from '~/components/Button/FollowButton'
|
||||
// import FollowList from '~/components/features/ProfileList/FollowList'
|
||||
import JoinLeaveButton from '~/components/Button/JoinLeaveButton'
|
||||
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
|
||||
import ProfileList from '~/components/features/ProfileList/ProfileList'
|
||||
// import SocialMedia from '~/components/SocialMedia/SocialMedia'
|
||||
// import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
|
||||
|
||||
// const tabToFilterMapping = ({ tab, id }) => {
|
||||
// return {
|
||||
@ -204,18 +262,19 @@ import { updateGroupMutation } from '~/graphql/groups.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// SocialMedia,
|
||||
PostTeaser,
|
||||
// HcFollowButton,
|
||||
// HcCountTo,
|
||||
// HcBadges,
|
||||
HcEmpty,
|
||||
ProfileAvatar,
|
||||
// ContentMenu,
|
||||
AvatarUploader,
|
||||
// ContentMenu,
|
||||
CountTo,
|
||||
Empty,
|
||||
// FollowButton,
|
||||
// FollowList,
|
||||
JoinLeaveButton,
|
||||
PostTeaser,
|
||||
ProfileAvatar,
|
||||
ProfileList,
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
// FollowList,
|
||||
// SocialMedia,
|
||||
// TabNavigation,
|
||||
},
|
||||
mixins: [postListActions],
|
||||
@ -223,31 +282,45 @@ export default {
|
||||
name: 'slide-up',
|
||||
mode: 'out-in',
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.groupName,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
// const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
|
||||
return {
|
||||
Group: [],
|
||||
GroupMembers: [],
|
||||
posts: [],
|
||||
hasMore: true,
|
||||
offset: 0,
|
||||
pageSize: 6,
|
||||
tabActive: 'post',
|
||||
// hasMore: true,
|
||||
// offset: 0,
|
||||
// pageSize: 6,
|
||||
// tabActive: 'post',
|
||||
// filter,
|
||||
followedByCountStartValue: 0,
|
||||
followedByCount: 7,
|
||||
followingCount: 7,
|
||||
// followedByCountStartValue: 0,
|
||||
// followedByCount: 7,
|
||||
// followingCount: 7,
|
||||
membersCountStartValue: 0,
|
||||
membersCountToLoad: Infinity,
|
||||
updateGroupMutation,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isGroupOwner() {
|
||||
return this.group.myRole === 'owner'
|
||||
},
|
||||
isGroupMember() {
|
||||
return this.group.myRole
|
||||
currentUser() {
|
||||
return this.$store.getters['auth/user']
|
||||
},
|
||||
group() {
|
||||
return this.Group ? this.Group[0] : {}
|
||||
return this.Group[0] ? this.Group[0] : {}
|
||||
},
|
||||
groupMembers() {
|
||||
return this.GroupMembers ? this.GroupMembers : []
|
||||
},
|
||||
isGroupOwner() {
|
||||
return this.group ? this.group.myRole === 'owner' : false
|
||||
},
|
||||
isGroupMember() {
|
||||
return this.group ? !!this.group.myRole : false
|
||||
},
|
||||
groupName() {
|
||||
const { name } = this.group || {}
|
||||
@ -384,10 +457,17 @@ export default {
|
||||
// this.user.followedByCurrentUser = followedByCurrentUser
|
||||
// this.user.followedBy = followedBy
|
||||
// },
|
||||
// fetchAllConnections(type) {
|
||||
// if (type === 'following') this.followingCount = Infinity
|
||||
// if (type === 'followedBy') this.followedByCount = Infinity
|
||||
// },
|
||||
prepareJoinLeave() {
|
||||
// "membersCountStartValue" is updated to avoid counting from 0 when join/leave
|
||||
this.membersCountStartValue = this.GroupMembers.length
|
||||
},
|
||||
updateJoinLeave({ myRoleInGroup }) {
|
||||
this.Group[0].myRole = myRoleInGroup
|
||||
this.$apollo.queries.GroupMembers.refetch()
|
||||
},
|
||||
fetchAllMembers() {
|
||||
this.membersCountToLoad = Infinity
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
// profilePagePosts: {
|
||||
@ -421,6 +501,17 @@ export default {
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
GroupMembers: {
|
||||
query() {
|
||||
return groupMembersQuery
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.id,
|
||||
}
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -449,4 +540,11 @@ export default {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
}
|
||||
.centered-text {
|
||||
text-align: center;
|
||||
margin-bottom: $space-xxx-small;
|
||||
}
|
||||
.chip {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -172,10 +172,10 @@
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import HcFollowButton from '~/components/FollowButton.vue'
|
||||
import HcFollowButton from '~/components/Button/FollowButton'
|
||||
import HcCountTo from '~/components/CountTo.vue'
|
||||
import HcBadges from '~/components/Badges.vue'
|
||||
import FollowList from '~/components/features/FollowList/FollowList'
|
||||
import FollowList, { followListVisibleCount } from '~/components/features/ProfileList/FollowList'
|
||||
import HcEmpty from '~/components/Empty/Empty'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import AvatarUploader from '~/components/Uploader/AvatarUploader'
|
||||
@ -220,6 +220,11 @@ export default {
|
||||
name: 'slide-up',
|
||||
mode: 'out-in',
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.userName,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
|
||||
return {
|
||||
@ -231,8 +236,8 @@ export default {
|
||||
tabActive: 'post',
|
||||
filter,
|
||||
followedByCountStartValue: 0,
|
||||
followedByCount: 7,
|
||||
followingCount: 7,
|
||||
followedByCount: followListVisibleCount,
|
||||
followingCount: followListVisibleCount,
|
||||
updateUserMutation: updateUserMutation(),
|
||||
}
|
||||
},
|
||||
|
||||
@ -4481,10 +4481,10 @@ neo4j-driver@^4.2.2, neo4j-driver@^4.3.4:
|
||||
neo4j-driver-core "^4.3.4"
|
||||
rxjs "^6.6.3"
|
||||
|
||||
neode@^0.4.7:
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.7.tgz#033007b57a2ee167e9ee5537493086db08d005eb"
|
||||
integrity sha512-YXlc187JRpeKCBcUIkY6nimXXG+Tvlopfe71/FPno2THrwmYt5mm0RPHZ+mXF2O1Xg6zvjKvOpCpDz2vHBfroQ==
|
||||
neode@^0.4.8:
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.8.tgz#0889b4fc7f1bf0b470b01fa5b8870373b5d47ad6"
|
||||
integrity sha512-pb91NfCOg4Fj5o+98H+S2XYC+ByQfbdhwcc1UVuzuUQ0Ezzj+jWz8NmKWU8ZfCH6l4plk71yDAPd2eTwpt+Xvg==
|
||||
dependencies:
|
||||
"@hapi/joi" "^15.1.1"
|
||||
dotenv "^4.0.0"
|
||||
|
||||