Merge branch 'master' into merge-5750-in-5818
107
CONTRIBUTING.md
@ -105,7 +105,7 @@ Sprint retrospective
|
|||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
|
We practice [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
|
||||||
|
|
||||||
* developers can make contributions to other people's PRs (after checking in with them)
|
* developers can make contributions to other people's PRs (after checking in with them)
|
||||||
* we avoid blocking because someone else isn't working, so we sometimes take over PRs from other developers
|
* we avoid blocking because someone else isn't working, so we sometimes take over PRs from other developers
|
||||||
@ -115,7 +115,7 @@ We believe in open source contributions as a learning experience – everyone is
|
|||||||
|
|
||||||
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
|
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
|
||||||
|
|
||||||
As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
|
As a volunteer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
## Open-Source Bounties
|
## Open-Source Bounties
|
||||||
@ -149,3 +149,106 @@ Our Open-Source bounty program is a work-in-progress. Based on our future
|
|||||||
experience we will make changes and improvements. So keep an eye on this
|
experience we will make changes and improvements. So keep an eye on this
|
||||||
contribution guide.
|
contribution guide.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## Programming
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
#### Quotation Marks
|
||||||
|
|
||||||
|
The following characters are different from the programming quotation mark:
|
||||||
|
|
||||||
|
`"` or `\"`
|
||||||
|
|
||||||
|
Please copy and paste the following quotes for the languages:
|
||||||
|
|
||||||
|
* de: „Dies ist ein Beispielsatz.“
|
||||||
|
* en: “This is a sample sentence.”
|
||||||
|
* See <https://grammar.collinsdictionary.com/easy-learning/when-do-you-use-quotation-marks-or-in-english>
|
||||||
|
|
||||||
|
## Docker – More Closely
|
||||||
|
|
||||||
|
### Apple M1 Platform
|
||||||
|
|
||||||
|
***Attention:** For using Docker commands in Apple M1 environments!*
|
||||||
|
|
||||||
|
#### Environment Variable For Apple M1 Platform
|
||||||
|
|
||||||
|
To set the Docker platform environment variable in your terminal tab, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# set env variable for your shell
|
||||||
|
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose Override File For Apple M1 Platform
|
||||||
|
|
||||||
|
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in main folder
|
||||||
|
|
||||||
|
# for development
|
||||||
|
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
||||||
|
# only once: init admin user and create indexes and contraints in Neo4j database
|
||||||
|
$ docker compose exec backend yarn prod:migrate init
|
||||||
|
# clean db
|
||||||
|
$ docker compose exec backend yarn db:reset
|
||||||
|
# seed db
|
||||||
|
$ docker compose exec backend yarn db:seed
|
||||||
|
|
||||||
|
# for production
|
||||||
|
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
||||||
|
# only once: init admin user and create indexes and contraints in Neo4j database
|
||||||
|
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analyzing Docker Builds
|
||||||
|
|
||||||
|
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
|
||||||
|
|
||||||
|
The `dive build` command is exactly the right one to fulfill what we are looking for.
|
||||||
|
We can use it just like the `docker build` command and get an analysis afterwards.
|
||||||
|
|
||||||
|
So, in our main folder, we use it in the following way:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in main folder
|
||||||
|
$ dive build --target <layer-name> -t "ocelotsocialnetwork/<app-name>:local-<layer-name>" --build-arg BBUILD_DATE="<build-date>" --build-arg BBUILD_VERSION="<build-version>" --build-arg BBUILD_COMMIT="<build-commit>" <app-folder-name-or-dot>/
|
||||||
|
```
|
||||||
|
|
||||||
|
The build arguments are optional.
|
||||||
|
|
||||||
|
For the specific applications, we use them as follows.
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
##### Production For Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in main folder
|
||||||
|
$ dive build --target production -t "ocelotsocialnetwork/backend:local-production" backend/
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Development For Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in main folder
|
||||||
|
$ dive build --target development -t "ocelotsocialnetwork/backend:local-development" backend/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Webapp
|
||||||
|
|
||||||
|
##### Production For Webapp
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in main folder
|
||||||
|
$ dive build --target production -t "ocelotsocialnetwork/webapp:local-production" webapp/
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Development For Webapp
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in main folder
|
||||||
|
$ dive build --target development -t "ocelotsocialnetwork/webapp:local-development" webapp/
|
||||||
|
```
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
# Docker More Closely
|
|
||||||
|
|
||||||
## Apple M1 Platform
|
|
||||||
|
|
||||||
***Attention:** For using Docker commands in Apple M1 environments!*
|
|
||||||
|
|
||||||
### Enviroment Variable For Apple M1 Platform
|
|
||||||
|
|
||||||
To set the Docker platform environment variable in your terminal tab, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# set env variable for your shell
|
|
||||||
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose Override File For Apple M1 Platform
|
|
||||||
|
|
||||||
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in main folder
|
|
||||||
|
|
||||||
# for development
|
|
||||||
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
|
||||||
# only once: init admin user and create indexes and contraints in Neo4j database
|
|
||||||
$ docker compose exec backend yarn prod:migrate init
|
|
||||||
# clean db
|
|
||||||
$ docker compose exec backend yarn db:reset
|
|
||||||
# seed db
|
|
||||||
$ docker compose exec backend yarn db:seed
|
|
||||||
|
|
||||||
# for production
|
|
||||||
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
|
||||||
# only once: init admin user and create indexes and contraints in Neo4j database
|
|
||||||
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Analysing Docker Builds
|
|
||||||
|
|
||||||
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
|
|
||||||
|
|
||||||
The `dive build` command is exactly the right one to fulfill what we are looking for.
|
|
||||||
We can use it just like the `docker build` command and get an analysis afterwards.
|
|
||||||
|
|
||||||
So, in our main folder, we use it in the following way:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in main folder
|
|
||||||
$ dive build --target <layer-name> -t "ocelotsocialnetwork/<app-name>:local-<layer-name>" --build-arg BBUILD_DATE="<build-date>" --build-arg BBUILD_VERSION="<build-version>" --build-arg BBUILD_COMMIT="<build-commit>" <app-folder-name-or-dot>/
|
|
||||||
```
|
|
||||||
|
|
||||||
The build arguments are optional.
|
|
||||||
|
|
||||||
For the specific applications, we use them as follows.
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
#### Production For Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in main folder
|
|
||||||
$ dive build --target production -t "ocelotsocialnetwork/backend:local-production" backend/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Development For Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in main folder
|
|
||||||
$ dive build --target development -t "ocelotsocialnetwork/backend:local-development" backend/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Webapp
|
|
||||||
|
|
||||||
#### Production For Webapp
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in main folder
|
|
||||||
$ dive build --target production -t "ocelotsocialnetwork/webapp:local-production" webapp/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Development For Webapp
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in main folder
|
|
||||||
$ dive build --target development -t "ocelotsocialnetwork/webapp:local-development" webapp/
|
|
||||||
```
|
|
||||||
@ -1 +1 @@
|
|||||||
v12.19.0
|
v19.4.0
|
||||||
@ -1,7 +1,7 @@
|
|||||||
##################################################################################
|
##################################################################################
|
||||||
# BASE (Is pushed to DockerHub for rebranding) ###################################
|
# BASE (Is pushed to DockerHub for rebranding) ###################################
|
||||||
##################################################################################
|
##################################################################################
|
||||||
FROM node:12.19.0-alpine3.10 as base
|
FROM node:19.4.0-alpine3.17 as base
|
||||||
|
|
||||||
# ENVs
|
# ENVs
|
||||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||||
|
|||||||
@ -19,12 +19,19 @@ Wait a little until your backend is up and running at [http://localhost:4000/](h
|
|||||||
## Installation without Docker
|
## Installation without Docker
|
||||||
|
|
||||||
For the local installation you need a recent version of
|
For the local installation you need a recent version of
|
||||||
[node](https://nodejs.org/en/) (>= `v10.12.0`). We are using
|
[Node](https://nodejs.org/en/) (>= `v16.19.0`). We are using
|
||||||
`12.19.0` and therefore we recommend to use the same version
|
`v19.4.0` and therefore we recommend to use the same version
|
||||||
([see](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4082)
|
([see](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4082)
|
||||||
some known problems with more recent node versions). You can use the
|
some known problems with more recent node versions). You can use the
|
||||||
[node version manager](https://github.com/nvm-sh/nvm) to switch
|
[node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch
|
||||||
between different local node versions.
|
between different local Node versions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install Node
|
||||||
|
$ cd backend
|
||||||
|
$ nvm install v19.4.0
|
||||||
|
$ nvm use v19.4.0
|
||||||
|
```
|
||||||
|
|
||||||
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||||
|
|
||||||
@ -32,6 +39,10 @@ Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
|||||||
# in main folder
|
# in main folder
|
||||||
$ cd backend
|
$ cd backend
|
||||||
$ yarn install
|
$ yarn install
|
||||||
|
# or just
|
||||||
|
$ yarn
|
||||||
|
# or just later on to use version of ".nvmrc" file
|
||||||
|
$ nvm use && yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy Environment Variables:
|
Copy Environment Variables:
|
||||||
|
|||||||
@ -10,4 +10,5 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
coverageReporters: ['lcov', 'text'],
|
coverageReporters: ['lcov', 'text'],
|
||||||
testMatch: ['**/src/**/?(*.)+(spec|test).js?(x)'],
|
testMatch: ['**/src/**/?(*.)+(spec|test).js?(x)'],
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/test/setup.js']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,22 +65,22 @@
|
|||||||
"linkifyjs": "~2.1.8",
|
"linkifyjs": "~2.1.8",
|
||||||
"lodash": "~4.17.14",
|
"lodash": "~4.17.14",
|
||||||
"merge-graphql-schemas": "^1.7.8",
|
"merge-graphql-schemas": "^1.7.8",
|
||||||
"metascraper": "^5.11.8",
|
"metascraper": "^5.33.5",
|
||||||
"metascraper-audio": "^5.14.26",
|
"metascraper-audio": "^5.33.5",
|
||||||
"metascraper-author": "^5.14.22",
|
"metascraper-author": "^5.33.5",
|
||||||
"metascraper-clearbit-logo": "^5.3.0",
|
"metascraper-clearbit-logo": "^5.3.0",
|
||||||
"metascraper-date": "^5.11.8",
|
"metascraper-date": "^5.33.5",
|
||||||
"metascraper-description": "^5.23.1",
|
"metascraper-description": "^5.33.5",
|
||||||
"metascraper-image": "^5.11.8",
|
"metascraper-image": "^5.33.5",
|
||||||
"metascraper-lang": "^5.23.1",
|
"metascraper-lang": "^5.33.5",
|
||||||
"metascraper-lang-detector": "^4.10.2",
|
"metascraper-lang-detector": "^4.10.2",
|
||||||
"metascraper-logo": "^5.14.26",
|
"metascraper-logo": "^5.33.5",
|
||||||
"metascraper-publisher": "^5.23.0",
|
"metascraper-publisher": "^5.33.5",
|
||||||
"metascraper-soundcloud": "^5.23.0",
|
"metascraper-soundcloud": "^5.33.5",
|
||||||
"metascraper-title": "^5.11.8",
|
"metascraper-title": "^5.33.5",
|
||||||
"metascraper-url": "^5.14.26",
|
"metascraper-url": "^5.33.5",
|
||||||
"metascraper-video": "^5.11.8",
|
"metascraper-video": "^5.33.5",
|
||||||
"metascraper-youtube": "^5.23.0",
|
"metascraper-youtube": "^5.33.5",
|
||||||
"migrate": "^1.7.0",
|
"migrate": "^1.7.0",
|
||||||
"mime-types": "^2.1.26",
|
"mime-types": "^2.1.26",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.0.4",
|
||||||
@ -123,6 +123,8 @@
|
|||||||
"supertest": "~4.0.2"
|
"supertest": "~4.0.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"fs-capacitor": "6.0.0"
|
"**/**/fs-capacitor":"^6.2.0",
|
||||||
|
"**/graphql-upload": "^11.0.0",
|
||||||
|
"nan": "2.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,7 +85,7 @@ class Store {
|
|||||||
await createDefaultAdminUser(session)
|
await createDefaultAdminUser(session)
|
||||||
if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session)
|
if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session)
|
||||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
|
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and constraints
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
[
|
[
|
||||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||||
|
|||||||
@ -10,10 +10,13 @@ const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
|
|||||||
const { inviteCode } = args
|
const { inviteCode } = args
|
||||||
const response = await resolve(root, args, context, resolveInfo)
|
const response = await resolve(root, args, context, resolveInfo)
|
||||||
const { email, nonce } = response
|
const { email, nonce } = response
|
||||||
if (inviteCode) {
|
if (nonce) {
|
||||||
await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } }))
|
// emails that already exist do not have a nonce
|
||||||
} else {
|
if (inviteCode) {
|
||||||
await sendMail(signupTemplate({ email, variables: { nonce } }))
|
await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } }))
|
||||||
|
} else {
|
||||||
|
await sendMail(signupTemplate({ email, variables: { nonce } }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delete response.nonce
|
delete response.nonce
|
||||||
return response
|
return response
|
||||||
@ -30,7 +33,9 @@ const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo)
|
|||||||
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
|
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
|
||||||
const response = await resolve(root, args, context, resolveInfo)
|
const response = await resolve(root, args, context, resolveInfo)
|
||||||
const { email, nonce, name } = response
|
const { email, nonce, name } = response
|
||||||
await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } }))
|
if (nonce) {
|
||||||
|
await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } }))
|
||||||
|
}
|
||||||
delete response.nonce
|
delete response.nonce
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
7
backend/src/schema/resolvers/Upload.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { GraphQLUpload } from 'graphql-upload'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// This maps the `Upload` scalar to the implementation provided
|
||||||
|
// by the `graphql-upload` package.
|
||||||
|
Upload: GraphQLUpload,
|
||||||
|
}
|
||||||
@ -40,7 +40,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check email does not belong to anybody
|
// check email does not belong to anybody
|
||||||
await existingEmailAddress({ args, context })
|
const existingEmail = await existingEmailAddress({ args, context })
|
||||||
|
if (existingEmail && existingEmail.alreadyExistingEmail && existingEmail.user)
|
||||||
|
return existingEmail.alreadyExistingEmail
|
||||||
|
|
||||||
const nonce = generateNonce()
|
const nonce = generateNonce()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -134,11 +134,17 @@ describe('AddEmailAddress', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('but if another user owns an `EmailAddress` already with that email', () => {
|
describe('but if another user owns an `EmailAddress` already with that email', () => {
|
||||||
it('throws UserInputError because of unique constraints', async () => {
|
it('does not throw UserInputError', async () => {
|
||||||
await Factory.build('user', {}, { email: 'new-email@example.org' })
|
await Factory.build('user', {}, { email: 'new-email@example.org' })
|
||||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||||
data: { AddEmailAddress: null },
|
data: {
|
||||||
errors: [{ message: 'A user account with this email already exists.' }],
|
AddEmailAddress: {
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
verifiedAt: null,
|
||||||
|
email: 'new-email@example.org',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -96,7 +96,7 @@ describe('Query', () => {
|
|||||||
description: null,
|
description: null,
|
||||||
html: null,
|
html: null,
|
||||||
image: null,
|
image: null,
|
||||||
lang: null,
|
lang: 'false',
|
||||||
publisher: null,
|
publisher: null,
|
||||||
sources: ['resource'],
|
sources: ['resource'],
|
||||||
title: null,
|
title: null,
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { UserInputError } from 'apollo-server'
|
|
||||||
|
|
||||||
export default async function alreadyExistingMail({ args, context }) {
|
export default async function alreadyExistingMail({ args, context }) {
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
try {
|
try {
|
||||||
@ -20,9 +18,11 @@ export default async function alreadyExistingMail({ args, context }) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
const [emailBelongsToUser] = await existingEmailAddressTxPromise
|
const [emailBelongsToUser] = await existingEmailAddressTxPromise
|
||||||
const { alreadyExistingEmail, user } = emailBelongsToUser || {}
|
/*
|
||||||
if (user) throw new UserInputError('A user account with this email already exists.')
|
const { alreadyExistingEmail, user } =
|
||||||
return alreadyExistingEmail
|
if (user) throw new UserInputError('A user account with this email already exists.')
|
||||||
|
*/
|
||||||
|
return emailBelongsToUser || {}
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,10 +113,11 @@ const sanitizeRelationshipType = (relationshipType) => {
|
|||||||
const localFileUpload = ({ createReadStream, uniqueFilename }) => {
|
const localFileUpload = ({ createReadStream, uniqueFilename }) => {
|
||||||
const destination = `/uploads/${uniqueFilename}`
|
const destination = `/uploads/${uniqueFilename}`
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
createReadStream()
|
createReadStream().pipe(
|
||||||
.pipe(createWriteStream(`public${destination}`))
|
createWriteStream(`public${destination}`)
|
||||||
.on('finish', () => resolve(destination))
|
.on('finish', () => resolve(destination))
|
||||||
.on('error', reject),
|
.on('error', (error) => reject(error)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,12 @@ export default {
|
|||||||
args.nonce = generateNonce()
|
args.nonce = generateNonce()
|
||||||
args.email = normalizeEmail(args.email)
|
args.email = normalizeEmail(args.email)
|
||||||
let emailAddress = await existingEmailAddress({ args, context })
|
let emailAddress = await existingEmailAddress({ args, context })
|
||||||
if (emailAddress) return emailAddress
|
/*
|
||||||
|
if (emailAddress.user) {
|
||||||
|
// what to do?
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (emailAddress.alreadyExistingEmail) return emailAddress.alreadyExistingEmail
|
||||||
try {
|
try {
|
||||||
emailAddress = await neode.create('EmailAddress', args)
|
emailAddress = await neode.create('EmailAddress', args)
|
||||||
return emailAddress.toJson()
|
return emailAddress.toJson()
|
||||||
|
|||||||
@ -118,9 +118,9 @@ describe('Signup', () => {
|
|||||||
await emailAddress.relateTo(user, 'belongsTo')
|
await emailAddress.relateTo(user, 'belongsTo')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws UserInputError error because of unique constraint violation', async () => {
|
it('does not throw UserInputError error', async () => {
|
||||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||||
errors: [{ message: 'A user account with this email already exists.' }],
|
data: { Signup: { email: 'someuser@example.org' } },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { RedisPubSub } from 'graphql-redis-subscriptions'
|
|||||||
import { PubSub } from 'graphql-subscriptions'
|
import { PubSub } from 'graphql-subscriptions'
|
||||||
import Redis from 'ioredis'
|
import Redis from 'ioredis'
|
||||||
import bodyParser from 'body-parser'
|
import bodyParser from 'body-parser'
|
||||||
|
import { graphqlUploadExpress } from 'graphql-upload'
|
||||||
|
|
||||||
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
||||||
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
|
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
|
||||||
@ -67,6 +68,7 @@ const createServer = (options) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
debug: !!CONFIG.DEBUG,
|
debug: !!CONFIG.DEBUG,
|
||||||
|
uploads: false,
|
||||||
tracing: !!CONFIG.DEBUG,
|
tracing: !!CONFIG.DEBUG,
|
||||||
formatError: (error) => {
|
formatError: (error) => {
|
||||||
if (error.message === 'ERROR_VALIDATION') {
|
if (error.message === 'ERROR_VALIDATION') {
|
||||||
@ -85,6 +87,7 @@ const createServer = (options) => {
|
|||||||
app.use(express.static('public'))
|
app.use(express.static('public'))
|
||||||
app.use(bodyParser.json({ limit: '10mb' }))
|
app.use(bodyParser.json({ limit: '10mb' }))
|
||||||
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }))
|
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }))
|
||||||
|
app.use(graphqlUploadExpress())
|
||||||
server.applyMiddleware({ app, path: '/' })
|
server.applyMiddleware({ app, path: '/' })
|
||||||
const httpServer = http.createServer(app)
|
const httpServer = http.createServer(app)
|
||||||
server.installSubscriptionHandlers(httpServer)
|
server.installSubscriptionHandlers(httpServer)
|
||||||
|
|||||||
8
backend/test/setup.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Polyfill missing encoders in jsdom
|
||||||
|
// https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest
|
||||||
|
import { TextEncoder, TextDecoder } from 'util'
|
||||||
|
global.TextEncoder = TextEncoder
|
||||||
|
global.TextDecoder = TextDecoder
|
||||||
|
|
||||||
|
// Metascraper takes longer nowadays, double time
|
||||||
|
jest.setTimeout(10000)
|
||||||
1336
backend/yarn.lock
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"projectId": "qa7fe2",
|
"projectId": "qa7fe2",
|
||||||
|
"defaultCommandTimeout": 10000,
|
||||||
"ignoreTestFiles": "*.js",
|
"ignoreTestFiles": "*.js",
|
||||||
"chromeWebSecurity": false,
|
"chromeWebSecurity": false,
|
||||||
"baseUrl": "http://localhost:3000",
|
"baseUrl": "http://localhost:3000",
|
||||||
|
|||||||
@ -13,29 +13,29 @@ Feature: User profile - list social media accounts
|
|||||||
When I navigate to page "/settings/my-social-media"
|
When I navigate to page "/settings/my-social-media"
|
||||||
Then I am on page "/settings/my-social-media"
|
Then I am on page "/settings/my-social-media"
|
||||||
When I add a social media link
|
When I add a social media link
|
||||||
Then I see a toaster with "Added social media"
|
Then I see a toaster with status "success"
|
||||||
And the new social media link shows up on the page
|
And the new social media link shows up on the page
|
||||||
|
|
||||||
Scenario: Other users viewing my Social Media
|
Scenario: Other users viewing my Social Media
|
||||||
Given I have added a social media link
|
Given I have added the social media link "https://freeradical.zone/peter-pan"
|
||||||
When I navigate to page "/profile/peter-pan"
|
When I navigate to page "/profile/peter-pan"
|
||||||
Then they should be able to see my social media links
|
Then they should be able to see my social media links
|
||||||
|
|
||||||
Scenario: Deleting Social Media
|
Scenario: Deleting Social Media
|
||||||
When I navigate to page "/settings/my-social-media"
|
When I navigate to page "/settings/my-social-media"
|
||||||
Then I am on page "/settings/my-social-media"
|
Then I am on page "/settings/my-social-media"
|
||||||
Given I have added a social media link
|
Given I have added the social media link "https://freeradical.zone/peter-pan"
|
||||||
When I delete a social media link
|
When I delete the social media link "https://freeradical.zone/peter-pan"
|
||||||
Then I see a toaster with "Deleted social media"
|
Then I see a toaster with status "success"
|
||||||
|
|
||||||
Scenario: Editing Social Media
|
Scenario: Editing Social Media
|
||||||
When I navigate to page "/settings/my-social-media"
|
When I navigate to page "/settings/my-social-media"
|
||||||
Then I am on page "/settings/my-social-media"
|
Then I am on page "/settings/my-social-media"
|
||||||
Given I have added a social media link
|
Given I have added the social media link "https://freeradical.zone/peter-pan"
|
||||||
When I start editing a social media link
|
When I start editing a social media link
|
||||||
Then I can cancel editing
|
Then I can cancel editing
|
||||||
When I start editing a social media link
|
When I start editing a social media link
|
||||||
And I edit and save the link
|
And I edit and save the link
|
||||||
Then I see a toaster with "Added social media"
|
Then I see a toaster with status "success"
|
||||||
And the new url is displayed
|
And the new url is displayed
|
||||||
But the old url is not displayed
|
But the old url is not displayed
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||||
|
|
||||||
When('I add a social media link', () => {
|
When('I add a social media link', () => {
|
||||||
cy.get('button')
|
cy.get('[data-test="add-save-button"]')
|
||||||
.contains('Add link')
|
|
||||||
.click()
|
.click()
|
||||||
.get('#editSocialMedia')
|
.get('#editSocialMedia')
|
||||||
.type('https://freeradical.zone/peter-pan')
|
.type('https://freeradical.zone/peter-pan')
|
||||||
.get('button')
|
.get('[data-test="add-save-button"]')
|
||||||
.contains('Save')
|
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
|
||||||
|
|
||||||
When('I delete a social media link', () => {
|
|
||||||
cy.get(".base-button[title='Delete']")
|
|
||||||
.click()
|
|
||||||
})
|
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||||
|
|
||||||
|
When('I delete the social media link {string}', (link) => {
|
||||||
|
cy.get('[data-test="delete-button"]')
|
||||||
|
.click()
|
||||||
|
cy.get('[data-test="confirm-modal"]')
|
||||||
|
.should("be.visible")
|
||||||
|
cy.get('[data-test="confirm-button"]')
|
||||||
|
.click()
|
||||||
|
cy.get('.ds-list-item-content > a')
|
||||||
|
.contains(link).should('not.exist')
|
||||||
|
})
|
||||||
@ -4,7 +4,6 @@ When('I edit and save the link', () => {
|
|||||||
cy.get('input#editSocialMedia')
|
cy.get('input#editSocialMedia')
|
||||||
.clear()
|
.clear()
|
||||||
.type('https://freeradical.zone/tinkerbell')
|
.type('https://freeradical.zone/tinkerbell')
|
||||||
.get('button')
|
.get('[data-test="add-save-button"]')
|
||||||
.contains('Save')
|
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { Given } from "cypress-cucumber-preprocessor/steps";
|
|
||||||
|
|
||||||
Given('I have added a social media link', () => {
|
|
||||||
cy.visit('/settings/my-social-media')
|
|
||||||
.get('button')
|
|
||||||
.contains('Add link')
|
|
||||||
.click()
|
|
||||||
.get('#editSocialMedia')
|
|
||||||
.type('https://freeradical.zone/peter-pan')
|
|
||||||
.get('button')
|
|
||||||
.contains('Save')
|
|
||||||
.click()
|
|
||||||
})
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { Given } from "cypress-cucumber-preprocessor/steps";
|
||||||
|
|
||||||
|
Given('I have added the social media link {string}', (link) => {
|
||||||
|
cy.visit('/settings/my-social-media')
|
||||||
|
.get('[data-test="add-save-button"]')
|
||||||
|
.click()
|
||||||
|
.get('#editSocialMedia')
|
||||||
|
.type(link)
|
||||||
|
.get('[data-test="add-save-button"]')
|
||||||
|
.click()
|
||||||
|
cy.get('.ds-list-item-content > a')
|
||||||
|
.contains(link)
|
||||||
|
})
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||||
|
|
||||||
When('I start editing a social media link', () => {
|
When('I start editing a social media link', () => {
|
||||||
cy.get(".base-button[title='Edit']")
|
cy.get('[data-test="edit-button"]')
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Then } from "cypress-cucumber-preprocessor/steps";
|
import { Then } from "cypress-cucumber-preprocessor/steps";
|
||||||
|
|
||||||
Then('they should be able to see my social media links', () => {
|
Then('they should be able to see my social media links', () => {
|
||||||
cy.get('.base-card')
|
cy.get('[data-test="social-media-list-headline"]')
|
||||||
.contains('Where else can I find Peter Pan?')
|
.contains('Peter Pan')
|
||||||
.get('a[href="https://freeradical.zone/peter-pan"]')
|
.get('a[href="https://freeradical.zone/peter-pan"]')
|
||||||
.should('have.length', 1)
|
.should('have.length', 1)
|
||||||
})
|
})
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Then } from "cypress-cucumber-preprocessor/steps";
|
||||||
|
|
||||||
|
Then("I see a toaster with status {string}", (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case "success":
|
||||||
|
cy.get(".iziToast.iziToast-color-green").should("be.visible");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -20,7 +20,6 @@ services:
|
|||||||
- GRAPHQL_URI=http://localhost:4000
|
- GRAPHQL_URI=http://localhost:4000
|
||||||
- CLIENT_URI=http://localhost:3000
|
- CLIENT_URI=http://localhost:3000
|
||||||
- JWT_SECRET=b/&&7b78BF&fv/Vd
|
- JWT_SECRET=b/&&7b78BF&fv/Vd
|
||||||
- MAPBOX_TOKEN=pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g
|
|
||||||
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
|
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
|
||||||
- NEO4J_apoc_import_file_enabled=true
|
- NEO4J_apoc_import_file_enabled=true
|
||||||
- "SSH_USERNAME=${SSH_USERNAME}"
|
- "SSH_USERNAME=${SSH_USERNAME}"
|
||||||
|
|||||||
@ -98,22 +98,22 @@ On a server with Kubernetes cluster:
|
|||||||
$ kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate init"
|
$ kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate init"
|
||||||
```
|
```
|
||||||
|
|
||||||
***Cypher commands to show indexes and contraints***
|
***Cypher commands to show indexes and constraints***
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# in browser command line or cypher shell
|
# in browser command line or cypher shell
|
||||||
|
|
||||||
# show all indexes and contraints
|
# show all indexes and constraints
|
||||||
$ :schema
|
$ :schema
|
||||||
|
|
||||||
# show all indexes
|
# show all indexes
|
||||||
$ CALL db.indexes();
|
$ CALL db.indexes();
|
||||||
|
|
||||||
# show all contraints
|
# show all constraints
|
||||||
$ CALL db.constraints();
|
$ CALL db.constraints();
|
||||||
```
|
```
|
||||||
|
|
||||||
***Cypher commands to create and drop indexes and contraints***
|
***Cypher commands to create and drop indexes and constraints***
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# in browser command line or cypher shell
|
# in browser command line or cypher shell
|
||||||
@ -126,6 +126,6 @@ $ CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"]);
|
|||||||
# drop an index
|
# drop an index
|
||||||
$ DROP CONSTRAINT ON ( image:Image ) ASSERT image.url IS UNIQUE
|
$ DROP CONSTRAINT ON ( image:Image ) ASSERT image.url IS UNIQUE
|
||||||
|
|
||||||
# drop all indexes and contraints
|
# drop all indexes and constraints
|
||||||
$ CALL apoc.schema.assert({},{},true) YIELD label, key RETURN * ;
|
$ CALL apoc.schema.assert({},{},true) YIELD label, key RETURN * ;
|
||||||
```
|
```
|
||||||
|
|||||||
@ -48,7 +48,8 @@
|
|||||||
"slug": "^6.0.0"
|
"slug": "^6.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"set-value": "^2.0.1"
|
"set-value": "^2.0.1",
|
||||||
|
"nan": "2.17.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
SENTRY_DSN_WEBAPP=
|
SENTRY_DSN_WEBAPP=
|
||||||
COMMIT=
|
COMMIT=
|
||||||
PUBLIC_REGISTRATION=false
|
|
||||||
INVITE_REGISTRATION=true
|
|
||||||
WEBSOCKETS_URI=ws://localhost:3000/api/graphql
|
WEBSOCKETS_URI=ws://localhost:3000/api/graphql
|
||||||
GRAPHQL_URI=http://localhost:4000/
|
GRAPHQL_URI=http://localhost:4000/
|
||||||
|
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
|
||||||
|
PUBLIC_REGISTRATION=false
|
||||||
|
INVITE_REGISTRATION=true
|
||||||
CATEGORIES_ACTIVE=false
|
CATEGORIES_ACTIVE=false
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
v16.19.0
|
v19.4.0
|
||||||
@ -1,7 +1,7 @@
|
|||||||
##################################################################################
|
##################################################################################
|
||||||
# BASE (Is pushed to DockerHub for rebranding) ###################################
|
# BASE (Is pushed to DockerHub for rebranding) ###################################
|
||||||
##################################################################################
|
##################################################################################
|
||||||
FROM node:16.19.0-alpine3.17 as base
|
FROM node:19.4.0-alpine3.17 as base
|
||||||
|
|
||||||
# ENVs
|
# ENVs
|
||||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
##################################################################################
|
##################################################################################
|
||||||
# BASE ###########################################################################
|
# BASE ###########################################################################
|
||||||
##################################################################################
|
##################################################################################
|
||||||
FROM node:16.19.0-alpine3.17 as base
|
FROM node:19.4.0-alpine3.17 as base
|
||||||
|
|
||||||
# ENVs
|
# ENVs
|
||||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||||
|
|||||||
@ -4,18 +4,32 @@
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
For preparation we need Node and recommend to use [node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch
|
||||||
|
between different local Node versions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install Node
|
||||||
|
$ cd webapp
|
||||||
|
$ nvm install v16.19.0
|
||||||
|
$ nvm use v16.19.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# install all dependencies
|
# install all dependencies
|
||||||
$ cd webapp/
|
$ cd webapp
|
||||||
$ yarn install
|
$ yarn install
|
||||||
# or just
|
# or just
|
||||||
$ yarn
|
$ yarn
|
||||||
|
# or just later on to use version of ".nvmrc" file
|
||||||
|
$ nvm use && yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy:
|
Copy:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
# in webapp/
|
# in webapp
|
||||||
cp .env.template .env
|
cp .env.template .env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
1
webapp/assets/_new/icons/svgs/globe-detailed.svg
Normal file
|
After Width: | Height: | Size: 121 KiB |
@ -3,28 +3,28 @@
|
|||||||
* @presenter Color
|
* @presenter Color
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$color-primary: rgb(23, 181, 63);
|
$color-primary: rgb(23, 181, 63);
|
||||||
$color-primary-light: rgb(96, 214, 98);
|
$color-primary-light: rgb(96, 214, 98);
|
||||||
$color-primary-dark: rgb(25, 122, 49);
|
$color-primary-dark: rgb(25, 122, 49);
|
||||||
$color-primary-active: rgb(25, 194, 67);
|
$color-primary-active: rgb(25, 194, 67);
|
||||||
$color-primary-inverse: rgb(241, 253, 244);
|
$color-primary-inverse: rgb(241, 253, 244);
|
||||||
$color-secondary: rgb(0, 142, 230);
|
$color-secondary: rgb(0, 142, 230);
|
||||||
$color-secondary-active: rgb(10, 161, 255);
|
$color-secondary-active: rgb(10, 161, 255);
|
||||||
$color-secondary-inverse: rgb(240, 249, 255);
|
$color-secondary-inverse: rgb(240, 249, 255);
|
||||||
$color-success: rgb(23, 181, 63);
|
$color-success: rgb(23, 181, 63);
|
||||||
$color-success-active: rgb(26, 203, 71);
|
$color-success-active: rgb(26, 203, 71);
|
||||||
$color-success-inverse: rgb(241, 253, 244);
|
$color-success-inverse: rgb(241, 253, 244);
|
||||||
$color-danger: rgb(219, 57, 36);
|
$color-danger: rgb(219, 57, 36);
|
||||||
$color-danger-light: rgb(242, 97, 65);
|
$color-danger-light: rgb(242, 97, 65);
|
||||||
$color-danger-dark: rgb(158, 43, 28);
|
$color-danger-dark: rgb(158, 43, 28);
|
||||||
$color-danger-active: rgb(224, 81, 62);
|
$color-danger-active: rgb(224, 81, 62);
|
||||||
$color-danger-inverse: rgb(253, 243, 242);
|
$color-danger-inverse: rgb(253, 243, 242);
|
||||||
$color-warning: rgb(230, 121, 25);
|
$color-warning: rgb(230, 121, 25);
|
||||||
$color-warning-active: rgb(233, 137, 53);
|
$color-warning-active: rgb(233, 137, 53);
|
||||||
$color-warning-inverse: rgb(253, 247, 241);
|
$color-warning-inverse: rgb(253, 247, 241);
|
||||||
$color-yellow: rgb(245, 196, 0);
|
$color-yellow: rgb(245, 196, 0);
|
||||||
$color-yellow-active: rgb(255, 206, 10);
|
$color-yellow-active: rgb(255, 206, 10);
|
||||||
$color-yellow-inverse: rgb(255, 252, 240);
|
$color-yellow-inverse: rgb(255, 252, 240);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @tokens Color Neutral
|
* @tokens Color Neutral
|
||||||
|
|||||||
@ -1,5 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link to="/groups"><base-button icon="users" circle ghost /></nuxt-link>
|
<nuxt-link to="/groups">
|
||||||
|
<base-button
|
||||||
|
icon="users"
|
||||||
|
circle
|
||||||
|
ghost
|
||||||
|
v-tooltip="{
|
||||||
|
content: $t('group.button.tooltip'),
|
||||||
|
placement: 'bottom-start',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
@change="changeGroupType($event)"
|
@change="changeGroupType($event)"
|
||||||
>
|
>
|
||||||
<option v-for="groupType in groupTypeOptions" :key="groupType" :value="groupType">
|
<option v-for="groupType in groupTypeOptions" :key="groupType" :value="groupType">
|
||||||
{{ $t(`group.types.${groupType}`) }}
|
{{ $t(`group.typesOptions.${groupType}`) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<ds-chip
|
<ds-chip
|
||||||
@ -142,25 +142,28 @@
|
|||||||
style="position: relative; display: inline-block; right: -96%; top: -33px; width: 26px"
|
style="position: relative; display: inline-block; right: -96%; top: -33px; width: 26px"
|
||||||
@click="formData.locationName = ''"
|
@click="formData.locationName = ''"
|
||||||
></base-button>
|
></base-button>
|
||||||
|
<ds-text class="location-hint" color="softer">
|
||||||
|
{{ $t('settings.data.labelCityHint') }}
|
||||||
|
</ds-text>
|
||||||
|
|
||||||
<ds-space margin-top="small" />
|
<ds-space margin-top="small" />
|
||||||
|
|
||||||
<!-- category -->
|
<!-- category -->
|
||||||
<categories-select
|
<div v-if="categoriesActive">
|
||||||
v-if="categoriesActive"
|
<ds-text class="select-label">
|
||||||
model="categoryIds"
|
{{ $t('group.categoriesTitle') }}
|
||||||
name="categoryIds"
|
</ds-text>
|
||||||
:existingCategoryIds="formData.categoryIds"
|
|
||||||
/>
|
|
||||||
<ds-chip
|
|
||||||
v-if="categoriesActive"
|
|
||||||
size="base"
|
|
||||||
:color="errors && errors.categoryIds ? 'danger' : 'medium'"
|
|
||||||
>
|
|
||||||
{{ formData.categoryIds.length }} / 3
|
|
||||||
<base-icon v-if="errors && errors.categoryIds" name="warning" />
|
|
||||||
</ds-chip>
|
|
||||||
|
|
||||||
|
<categories-select
|
||||||
|
model="categoryIds"
|
||||||
|
name="categoryIds"
|
||||||
|
:existingCategoryIds="formData.categoryIds"
|
||||||
|
/>
|
||||||
|
<ds-chip size="base" :color="errors && errors.categoryIds ? 'danger' : 'medium'">
|
||||||
|
{{ formData.categoryIds.length }} / 3
|
||||||
|
<base-icon v-if="errors && errors.categoryIds" name="warning" />
|
||||||
|
</ds-chip>
|
||||||
|
</div>
|
||||||
<!-- submit -->
|
<!-- submit -->
|
||||||
<ds-space margin-top="large">
|
<ds-space margin-top="large">
|
||||||
<nuxt-link to="/groups">
|
<nuxt-link to="/groups">
|
||||||
@ -436,5 +439,9 @@ export default {
|
|||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
margin-top: $space-base;
|
margin-top: $space-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .location-hint {
|
||||||
|
margin-top: -$space-base + $space-xxx-small;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
<logo logoType="header" />
|
<logo logoType="header" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<!-- dynamic-brand-menu -->
|
<!-- dynamic brand menus -->
|
||||||
<ds-flex-item
|
<ds-flex-item
|
||||||
v-for="item in menu"
|
v-for="item in menu"
|
||||||
:key="item.name"
|
:key="item.name"
|
||||||
@ -43,8 +43,7 @@
|
|||||||
</ds-text>
|
</ds-text>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
|
<!-- search field -->
|
||||||
<!-- search-field -->
|
|
||||||
<ds-flex-item
|
<ds-flex-item
|
||||||
v-if="isLoggedIn"
|
v-if="isLoggedIn"
|
||||||
id="nav-search-box"
|
id="nav-search-box"
|
||||||
@ -59,233 +58,254 @@
|
|||||||
>
|
>
|
||||||
<search-field />
|
<search-field />
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<!-- filter-menu
|
<!-- filter menu -->
|
||||||
TODO: Filter is only visible on index
|
<!-- TODO: Filter is only visible on index -->
|
||||||
-->
|
<ds-flex-item v-if="isLoggedIn && SHOW_CONTENT_FILTER_HEADER_MENU" style="flex-grow: 0; flex-basis: auto">
|
||||||
<ds-flex-item
|
<client-only>
|
||||||
v-if="isLoggedIn && SHOW_CONTENT_FILTER_HEADER_MENU"
|
<filter-menu v-show="showFilterMenuDropdown" />
|
||||||
style="flex-grow: 0; flex-basis: auto"
|
</client-only>
|
||||||
>
|
</ds-flex-item>
|
||||||
<client-only>
|
<!-- right symbols -->
|
||||||
<filter-menu v-show="showFilterMenuDropdown" />
|
<ds-flex-item style="flex-basis: auto">
|
||||||
</client-only>
|
<div class="main-navigation-right" style="flex-basis: auto">
|
||||||
</ds-flex-item>
|
<!-- locale switch -->
|
||||||
<!-- locale-switch -->
|
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||||
<ds-flex-item style="flex-basis: auto">
|
<template v-if="isLoggedIn">
|
||||||
<div class="main-navigation-right" style="flex-basis: auto">
|
<!-- notification menu -->
|
||||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
|
||||||
<template v-if="isLoggedIn">
|
|
||||||
<client-only>
|
|
||||||
<!-- notification-menu -->
|
|
||||||
<notification-menu placement="top" />
|
|
||||||
</client-only>
|
|
||||||
<div v-if="inviteRegistration">
|
|
||||||
<client-only>
|
<client-only>
|
||||||
<!-- invite-button -->
|
<notification-menu placement="top" />
|
||||||
<invite-button placement="top" />
|
|
||||||
</client-only>
|
</client-only>
|
||||||
</div>
|
<!-- invite button -->
|
||||||
<!-- group button -->
|
<div v-if="inviteRegistration">
|
||||||
<client-only v-if="SHOW_GROUP_BUTTON_IN_HEADER">
|
<client-only>
|
||||||
|
<invite-button placement="top" />
|
||||||
|
</client-only>
|
||||||
|
</div>
|
||||||
|
<!-- group button -->
|
||||||
|
<client-only v-if="SHOW_GROUP_BUTTON_IN_HEADER">
|
||||||
|
<group-button />
|
||||||
|
</client-only>
|
||||||
|
<!-- map button -->
|
||||||
|
<client-only v-if="!isEmpty(this.$env.MAPBOX_TOKEN)">
|
||||||
|
<map-button />
|
||||||
|
</client-only>
|
||||||
|
<!-- avatar menu -->
|
||||||
|
<client-only>
|
||||||
|
<avatar-menu placement="top" />
|
||||||
|
</client-only>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
|
||||||
|
<!-- mobile header menu -->
|
||||||
|
<div v-else class="mobil-header-box">
|
||||||
|
<!-- logo, hamburger-->
|
||||||
|
<ds-flex style="align-items: center">
|
||||||
|
<ds-flex-item :width="{ base: LOGOS.LOGO_HEADER_WIDTH }" style="margin-right: 20px">
|
||||||
|
<a
|
||||||
|
v-if="LOGOS.LOGO_HEADER_CLICK.externalLink"
|
||||||
|
:href="LOGOS.LOGO_HEADER_CLICK.externalLink.url"
|
||||||
|
:target="LOGOS.LOGO_HEADER_CLICK.externalLink.target"
|
||||||
|
>
|
||||||
|
<logo logoType="header" />
|
||||||
|
</a>
|
||||||
|
<nuxt-link
|
||||||
|
v-else
|
||||||
|
:to="LOGOS.LOGO_HEADER_CLICK.internalPath.to"
|
||||||
|
v-scroll-to="LOGOS.LOGO_HEADER_CLICK.internalPath.scrollTo"
|
||||||
|
>
|
||||||
|
<logo logoType="header" />
|
||||||
|
</nuxt-link>
|
||||||
|
</ds-flex-item>
|
||||||
|
|
||||||
|
<!-- mobile hamburger menu -->
|
||||||
|
<ds-flex-item class="mobile-hamburger-menu">
|
||||||
|
<client-only>
|
||||||
|
<div style="display: inline-flex; padding-right: 20px">
|
||||||
|
<notification-menu />
|
||||||
|
</div>
|
||||||
|
</client-only>
|
||||||
|
<base-button icon="bars" @click="toggleMobileMenuView" circle />
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
<!-- search, filter -->
|
||||||
|
<ds-flex class="mobile-menu">
|
||||||
|
<!-- search field mobile -->
|
||||||
|
<ds-flex-item
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
style="padding: 20px"
|
||||||
|
>
|
||||||
|
<search-field />
|
||||||
|
</ds-flex-item>
|
||||||
|
<!-- filter menu mobile -->
|
||||||
|
<ds-flex-item
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
style="flex-grow: 0; flex-basis: auto; padding: 20px 0"
|
||||||
|
>
|
||||||
|
<client-only>
|
||||||
|
<filter-menu v-show="showFilterMenuDropdown" :showMobileMenu="showMobileMenu" />
|
||||||
|
</client-only>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
<!-- right symbols -->
|
||||||
|
<ds-flex style="margin: 0 20px">
|
||||||
|
<!-- locale switch mobile -->
|
||||||
|
<ds-flex-item :class="{ 'hide-mobile-menu': !toggleMobileMenu }">
|
||||||
|
<locale-switch
|
||||||
|
class="topbar-locale-switch topbar-locale-switch-mobile"
|
||||||
|
placement="top"
|
||||||
|
offset="8"
|
||||||
|
/>
|
||||||
|
</ds-flex-item>
|
||||||
|
<!-- invite button mobile -->
|
||||||
|
<ds-flex-item
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
style="text-align: center"
|
||||||
|
>
|
||||||
|
<client-only>
|
||||||
|
<invite-button placement="top" />
|
||||||
|
</client-only>
|
||||||
|
</ds-flex-item>
|
||||||
|
<!-- group button -->
|
||||||
|
<ds-flex-item
|
||||||
|
v-if="SHOW_GROUP_BUTTON_IN_HEADER"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
style="text-align: center"
|
||||||
|
>
|
||||||
|
<client-only>
|
||||||
<group-button />
|
<group-button />
|
||||||
</client-only>
|
</client-only>
|
||||||
<!-- avatar-menu -->
|
</ds-flex-item>
|
||||||
|
<!-- map button -->
|
||||||
|
<ds-flex-item
|
||||||
|
v-if="!isEmpty(this.$env.MAPBOX_TOKEN)"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
style="text-align: center"
|
||||||
|
>
|
||||||
|
<client-only>
|
||||||
|
<map-button />
|
||||||
|
</client-only>
|
||||||
|
</ds-flex-item>
|
||||||
|
<!-- avatar menu mobile -->
|
||||||
|
<ds-flex-item :class="{ 'hide-mobile-menu': !toggleMobileMenu }" style="text-align: end">
|
||||||
<client-only>
|
<client-only>
|
||||||
<avatar-menu placement="top" />
|
<avatar-menu placement="top" />
|
||||||
</client-only>
|
</client-only>
|
||||||
</template>
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
<div :class="{ 'hide-mobile-menu': !toggleMobileMenu }" class="mobile-menu footer-mobile">
|
||||||
|
<!-- dynamic branding menus -->
|
||||||
|
<ul v-if="isHeaderMenu" class="dynamic-branding-mobil">
|
||||||
|
<li v-for="item in menu" :key="item.name">
|
||||||
|
<a v-if="item.url" :href="item.url" :target="item.target">
|
||||||
|
<ds-text size="large" bold>
|
||||||
|
{{ $t(item.nameIdent) }}
|
||||||
|
</ds-text>
|
||||||
|
</a>
|
||||||
|
<nuxt-link v-else :to="item.path">
|
||||||
|
<ds-text size="large" bold>
|
||||||
|
{{ $t(item.nameIdent) }}
|
||||||
|
</ds-text>
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<!-- dynamic footer menu in header -->
|
||||||
|
<ul class="dynamic-footer-mobil">
|
||||||
|
<li v-for="pageParams in links.FOOTER_LINK_LIST" :key="pageParams.name">
|
||||||
|
<page-params-link :pageParams="pageParams">
|
||||||
|
{{ $t(pageParams.internalPage.footerIdent) }}
|
||||||
|
</page-params-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</ds-flex-item>
|
|
||||||
</ds-flex>
|
|
||||||
|
|
||||||
<!-- mobile header menu -->
|
|
||||||
<div v-else class="mobil-header-box">
|
|
||||||
<!-- logo, hamburger-->
|
|
||||||
<ds-flex style="align-items: center">
|
|
||||||
<ds-flex-item :width="{ base: LOGOS.LOGO_HEADER_WIDTH }" style="margin-right: 20px">
|
|
||||||
<a
|
|
||||||
v-if="LOGOS.LOGO_HEADER_CLICK.externalLink"
|
|
||||||
:href="LOGOS.LOGO_HEADER_CLICK.externalLink.url"
|
|
||||||
:target="LOGOS.LOGO_HEADER_CLICK.externalLink.target"
|
|
||||||
>
|
|
||||||
<logo logoType="header" />
|
|
||||||
</a>
|
|
||||||
<nuxt-link
|
|
||||||
v-else
|
|
||||||
:to="LOGOS.LOGO_HEADER_CLICK.internalPath.to"
|
|
||||||
v-scroll-to="LOGOS.LOGO_HEADER_CLICK.internalPath.scrollTo"
|
|
||||||
>
|
|
||||||
<logo logoType="header" />
|
|
||||||
</nuxt-link>
|
|
||||||
</ds-flex-item>
|
|
||||||
|
|
||||||
<!-- mobile hamburger menu -->
|
|
||||||
<ds-flex-item class="mobile-hamburger-menu">
|
|
||||||
<client-only>
|
|
||||||
<div style="display: inline-flex; padding-right: 20px">
|
|
||||||
<notification-menu />
|
|
||||||
</div>
|
|
||||||
</client-only>
|
|
||||||
<base-button icon="bars" @click="toggleMobileMenuView" circle />
|
|
||||||
</ds-flex-item>
|
|
||||||
</ds-flex>
|
|
||||||
<!-- search, filter-->
|
|
||||||
<ds-flex class="mobile-menu">
|
|
||||||
<!-- search-field mobile-->
|
|
||||||
<ds-flex-item
|
|
||||||
v-if="isLoggedIn"
|
|
||||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
|
||||||
style="padding: 20px"
|
|
||||||
>
|
|
||||||
<search-field />
|
|
||||||
</ds-flex-item>
|
|
||||||
<!-- filter menu mobile-->
|
|
||||||
<ds-flex-item
|
|
||||||
v-if="isLoggedIn"
|
|
||||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
|
||||||
style="flex-grow: 0; flex-basis: auto; padding: 20px 0"
|
|
||||||
>
|
|
||||||
<client-only>
|
|
||||||
<filter-menu v-show="showFilterMenuDropdown" :showMobileMenu="showMobileMenu" />
|
|
||||||
</client-only>
|
|
||||||
</ds-flex-item>
|
|
||||||
</ds-flex>
|
|
||||||
<!-- switch language, notification, invite, profil -->
|
|
||||||
<ds-flex style="margin: 0 20px">
|
|
||||||
<!-- locale-switch mobile-->
|
|
||||||
<ds-flex-item :class="{ 'hide-mobile-menu': !toggleMobileMenu }">
|
|
||||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
|
||||||
</ds-flex-item>
|
|
||||||
<!-- invite-button mobile-->
|
|
||||||
<ds-flex-item
|
|
||||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
|
||||||
style="text-align: center"
|
|
||||||
>
|
|
||||||
<client-only>
|
|
||||||
<invite-button placement="top" />
|
|
||||||
</client-only>
|
|
||||||
</ds-flex-item>
|
|
||||||
<!-- group button -->
|
|
||||||
<ds-flex-item
|
|
||||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
|
||||||
style="text-align: center"
|
|
||||||
>
|
|
||||||
<client-only v-if="SHOW_GROUP_BUTTON_IN_HEADER">
|
|
||||||
<group-button />
|
|
||||||
</client-only>
|
|
||||||
</ds-flex-item>
|
|
||||||
<!-- avatar-menu mobile-->
|
|
||||||
<ds-flex-item :class="{ 'hide-mobile-menu': !toggleMobileMenu }" style="text-align: end">
|
|
||||||
<client-only>
|
|
||||||
<avatar-menu placement="top" />
|
|
||||||
</client-only>
|
|
||||||
</ds-flex-item>
|
|
||||||
</ds-flex>
|
|
||||||
<div :class="{ 'hide-mobile-menu': !toggleMobileMenu }" class="mobile-menu footer-mobile">
|
|
||||||
<!-- dynamic branding menu -->
|
|
||||||
<ul v-if="isHeaderMenu" class="dynamic-branding-mobil">
|
|
||||||
<li v-for="item in menu" :key="item.name">
|
|
||||||
<a v-if="item.url" :href="item.url" :target="item.target">
|
|
||||||
<ds-text size="large" bold>
|
|
||||||
{{ $t(item.nameIdent) }}
|
|
||||||
</ds-text>
|
|
||||||
</a>
|
|
||||||
<nuxt-link v-else :to="item.path">
|
|
||||||
<ds-text size="large" bold>
|
|
||||||
{{ $t(item.nameIdent) }}
|
|
||||||
</ds-text>
|
|
||||||
</nuxt-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<hr />
|
|
||||||
<!-- dynamic footer menu in header -->
|
|
||||||
<ul class="dynamic-footer-mobil">
|
|
||||||
<li v-for="pageParams in links.FOOTER_LINK_LIST" :key="pageParams.name">
|
|
||||||
<page-params-link :pageParams="pageParams">
|
|
||||||
{{ $t(pageParams.internalPage.footerIdent) }}
|
|
||||||
</page-params-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ds-container>
|
</ds-container>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
|
||||||
import { SHOW_CONTENT_FILTER_HEADER_MENU } from '~/constants/filter.js'
|
|
||||||
import LOGOS from '~/constants/logos.js'
|
|
||||||
import headerMenu from '~/constants/headerMenu.js'
|
|
||||||
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
|
|
||||||
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
|
||||||
import GroupButton from '~/components/Group/GroupButton'
|
|
||||||
import InviteButton from '~/components/InviteButton/InviteButton'
|
|
||||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
|
||||||
import Logo from '~/components/Logo/Logo'
|
|
||||||
import SearchField from '~/components/features/SearchField/SearchField.vue'
|
|
||||||
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
|
|
||||||
import links from '~/constants/links.js'
|
|
||||||
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
|
|
||||||
|
|
||||||
export default {
|
<script>
|
||||||
components: {
|
import { mapGetters } from 'vuex'
|
||||||
AvatarMenu,
|
import isEmpty from 'lodash/isEmpty'
|
||||||
FilterMenu,
|
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
||||||
GroupButton,
|
import { SHOW_CONTENT_FILTER_HEADER_MENU } from '~/constants/filter.js'
|
||||||
InviteButton,
|
import LOGOS from '~/constants/logos.js'
|
||||||
LocaleSwitch,
|
import headerMenu from '~/constants/headerMenu.js'
|
||||||
Logo,
|
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
|
||||||
NotificationMenu,
|
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
||||||
PageParamsLink,
|
import GroupButton from '~/components/Group/GroupButton'
|
||||||
SearchField,
|
import InviteButton from '~/components/InviteButton/InviteButton'
|
||||||
},
|
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||||
props: {
|
import Logo from '~/components/Logo/Logo'
|
||||||
showMobileMenu: { type: Boolean, default: false },
|
import MapButton from '~/components/Map/MapButton'
|
||||||
},
|
import SearchField from '~/components/features/SearchField/SearchField.vue'
|
||||||
data() {
|
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
|
||||||
return {
|
import links from '~/constants/links.js'
|
||||||
hideNavbar: false,
|
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
|
||||||
prevScrollpos: 0,
|
|
||||||
links,
|
export default {
|
||||||
LOGOS,
|
components: {
|
||||||
SHOW_GROUP_BUTTON_IN_HEADER,
|
AvatarMenu,
|
||||||
SHOW_CONTENT_FILTER_HEADER_MENU,
|
FilterMenu,
|
||||||
isHeaderMenu: headerMenu.MENU.length > 0,
|
GroupButton,
|
||||||
menu: headerMenu.MENU,
|
InviteButton,
|
||||||
mobileSearchVisible: false,
|
LocaleSwitch,
|
||||||
toggleMobileMenu: false,
|
Logo,
|
||||||
inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling,
|
MapButton,
|
||||||
categoriesActive: this.$env.CATEGORIES_ACTIVE,
|
NotificationMenu,
|
||||||
}
|
PageParamsLink,
|
||||||
},
|
SearchField,
|
||||||
computed: {
|
},
|
||||||
...mapGetters({
|
props: {
|
||||||
isLoggedIn: 'auth/isLoggedIn',
|
showMobileMenu: { type: Boolean, default: false },
|
||||||
}),
|
},
|
||||||
showFilterMenuDropdown() {
|
data() {
|
||||||
const [firstRoute] = this.$route.matched
|
return {
|
||||||
return firstRoute && firstRoute.name === 'index'
|
hideNavbar: false,
|
||||||
},
|
prevScrollpos: 0,
|
||||||
},
|
isEmpty,
|
||||||
methods: {
|
links,
|
||||||
handleScroll() {
|
LOGOS,
|
||||||
const currentScrollPos = window.pageYOffset
|
SHOW_GROUP_BUTTON_IN_HEADER,
|
||||||
if (this.prevScrollpos > currentScrollPos) {
|
SHOW_CONTENT_FILTER_HEADER_MENU,
|
||||||
this.hideNavbar = false
|
isHeaderMenu: headerMenu.MENU.length > 0,
|
||||||
} else {
|
menu: headerMenu.MENU,
|
||||||
this.hideNavbar = true
|
mobileSearchVisible: false,
|
||||||
}
|
toggleMobileMenu: false,
|
||||||
this.prevScrollpos = currentScrollPos
|
inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling,
|
||||||
},
|
categoriesActive: this.$env.CATEGORIES_ACTIVE,
|
||||||
toggleMobileMenuView() {
|
}
|
||||||
this.toggleMobileMenu = !this.toggleMobileMenu
|
},
|
||||||
},
|
computed: {
|
||||||
},
|
...mapGetters({
|
||||||
mounted() {
|
isLoggedIn: 'auth/isLoggedIn',
|
||||||
window.addEventListener('scroll', this.handleScroll)
|
}),
|
||||||
},
|
showFilterMenuDropdown() {
|
||||||
}
|
const [firstRoute] = this.$route.matched
|
||||||
|
return firstRoute && firstRoute.name === 'index'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleScroll() {
|
||||||
|
const currentScrollPos = window.pageYOffset
|
||||||
|
if (this.prevScrollpos > currentScrollPos) {
|
||||||
|
this.hideNavbar = false
|
||||||
|
} else {
|
||||||
|
this.hideNavbar = true
|
||||||
|
}
|
||||||
|
this.prevScrollpos = currentScrollPos
|
||||||
|
},
|
||||||
|
toggleMobileMenuView() {
|
||||||
|
this.toggleMobileMenu = !this.toggleMobileMenu
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -309,6 +329,9 @@ export default {
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
.topbar-locale-switch-mobile {
|
||||||
|
margin-top: $space-xx-small;
|
||||||
|
}
|
||||||
.main-navigation-flex {
|
.main-navigation-flex {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||||
<template #default="{ toggleMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<a class="locale-menu" href="#" @click.prevent="toggleMenu()">
|
<a class="locale-menu" href="#" @click.prevent="toggleMenu()">
|
||||||
<base-icon name="globe" />
|
<!-- <base-icon name="globe" /> -->
|
||||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
30
webapp/components/Map/MapButton.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nuxt-link to="/map">
|
||||||
|
<base-button
|
||||||
|
class="map-button"
|
||||||
|
circle
|
||||||
|
ghost
|
||||||
|
v-tooltip="{
|
||||||
|
content: $t('map.button.tooltip'),
|
||||||
|
placement: 'bottom-start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<base-icon name="globe-detailed" size="large" />
|
||||||
|
</base-button>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'MapButton',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.map-button {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
webapp/components/Map/MapStylesButtons.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<base-button
|
||||||
|
:class="['map-style-button', actualStyle === style.url ? '' : '--deactivated']"
|
||||||
|
v-for="style in styles"
|
||||||
|
:key="style.title"
|
||||||
|
filled
|
||||||
|
size="small"
|
||||||
|
@click="setStyle(style.url)"
|
||||||
|
>
|
||||||
|
{{ style.title }}
|
||||||
|
</base-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'MapStylesButtons',
|
||||||
|
props: {
|
||||||
|
styles: { type: Array, required: true },
|
||||||
|
actualStyle: { type: String, required: true },
|
||||||
|
setStyle: { type: Function, required: true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.map-style-button {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
|
||||||
|
&.--deactivated {
|
||||||
|
color: $text-color-base;
|
||||||
|
background-color: $background-color-softer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -22,6 +22,7 @@
|
|||||||
:name="name"
|
:name="name"
|
||||||
@close="close"
|
@close="close"
|
||||||
/>
|
/>
|
||||||
|
<!-- "id", "type", and "name" props are only used for compatibility with the other modals -->
|
||||||
<confirm-modal
|
<confirm-modal
|
||||||
v-if="open === 'confirm'"
|
v-if="open === 'confirm'"
|
||||||
:id="data.resource.id"
|
:id="data.resource.id"
|
||||||
@ -57,6 +58,7 @@ export default {
|
|||||||
open: 'modal/open',
|
open: 'modal/open',
|
||||||
}),
|
}),
|
||||||
name() {
|
name() {
|
||||||
|
// REFACTORING: This gets unneccesary if we use "modalData" in all modals by probaply replacing them all by "confirm-modal"
|
||||||
if (!this.data || !this.data.resource) return ''
|
if (!this.data || !this.data.resource) return ''
|
||||||
const {
|
const {
|
||||||
resource: { name, title, author },
|
resource: { name, title, author },
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
|
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel" data-test="confirm-modal">
|
||||||
<transition name="ds-transition-fade">
|
<transition name="ds-transition-fade">
|
||||||
<ds-flex v-if="success" class="hc-modal-success" centered>
|
<ds-flex v-if="success" class="hc-modal-success" centered>
|
||||||
<sweetalert-icon icon="success" />
|
<sweetalert-icon icon="success" />
|
||||||
@ -15,6 +15,7 @@
|
|||||||
:danger="!modalData.buttons.confirm.danger"
|
:danger="!modalData.buttons.confirm.danger"
|
||||||
:icon="modalData.buttons.cancel.icon"
|
:icon="modalData.buttons.cancel.icon"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
|
data-test="cancel-button"
|
||||||
>
|
>
|
||||||
{{ $t(modalData.buttons.cancel.textIdent) }}
|
{{ $t(modalData.buttons.cancel.textIdent) }}
|
||||||
</base-button>
|
</base-button>
|
||||||
@ -25,6 +26,7 @@
|
|||||||
:icon="modalData.buttons.confirm.icon"
|
:icon="modalData.buttons.confirm.icon"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="confirm"
|
@click="confirm"
|
||||||
|
data-test="confirm-button"
|
||||||
>
|
>
|
||||||
{{ $t(modalData.buttons.confirm.textIdent) }}
|
{{ $t(modalData.buttons.confirm.textIdent) }}
|
||||||
</base-button>
|
</base-button>
|
||||||
@ -41,10 +43,10 @@ export default {
|
|||||||
SweetalertIcon,
|
SweetalertIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
name: { type: String, default: '' },
|
name: { type: String, default: '' }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||||
type: { type: String, required: true },
|
type: { type: String, required: true }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||||
modalData: { type: Object, required: true },
|
modalData: { type: Object, required: true },
|
||||||
id: { type: String, required: true },
|
id: { type: String, required: true }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -30,7 +30,10 @@
|
|||||||
v-for="category in post.categories"
|
v-for="category in post.categories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t(`contribution.category.description.${category.slug}`),
|
content: `
|
||||||
|
${$t(`contribution.category.name.${category.slug}`)}:
|
||||||
|
${$t(`contribution.category.description.${category.slug}`)}
|
||||||
|
`,
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
:icon="category.icon"
|
:icon="category.icon"
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<ds-space v-if="user.socialMedia && user.socialMedia.length" margin="large">
|
<ds-space v-if="user.socialMedia && user.socialMedia.length" margin="large">
|
||||||
<base-card class="social-media-bc">
|
<base-card class="social-media-bc">
|
||||||
<ds-space margin="x-small">
|
<ds-space margin="x-small">
|
||||||
<ds-text tag="h5" color="soft">
|
<ds-text tag="h5" color="soft" data-test="social-media-list-headline">
|
||||||
{{ $t('profile.socialMedia') }} {{ userName | truncate(15) }}?
|
{{ $t('profile.socialMedia') }} {{ userName | truncate(15) }}?
|
||||||
</ds-text>
|
</ds-text>
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import MySomethingList from './MySomethingList.vue'
|
import MySomethingList from './MySomethingList.vue'
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
@ -9,12 +10,23 @@ describe('MySomethingList.vue', () => {
|
|||||||
let propsData
|
let propsData
|
||||||
let data
|
let data
|
||||||
let mocks
|
let mocks
|
||||||
|
let mutations
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData = {
|
propsData = {
|
||||||
useFormData: { dummy: '' },
|
useFormData: { dummy: '' },
|
||||||
useItems: [{ id: 'id', dummy: 'dummy' }],
|
useItems: [{ id: 'id', dummy: 'dummy' }],
|
||||||
namePropertyKey: 'dummy',
|
namePropertyKey: 'dummy',
|
||||||
|
texts: {
|
||||||
|
addButton: 'add-button',
|
||||||
|
addNew: 'add-new-something',
|
||||||
|
deleteModal: {
|
||||||
|
titleIdent: 'delete-modal.title',
|
||||||
|
messageIdent: 'delete-modal.message',
|
||||||
|
confirm: { icon: 'trash', buttonTextIdent: 'delete-modal.confirm-button' },
|
||||||
|
},
|
||||||
|
edit: 'edit-something',
|
||||||
|
},
|
||||||
callbacks: { edit: jest.fn(), submit: jest.fn(), delete: jest.fn() },
|
callbacks: { edit: jest.fn(), submit: jest.fn(), delete: jest.fn() },
|
||||||
}
|
}
|
||||||
data = () => {
|
data = () => {
|
||||||
@ -30,6 +42,9 @@ describe('MySomethingList.vue', () => {
|
|||||||
success: jest.fn(),
|
success: jest.fn(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
mutations = {
|
||||||
|
'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
@ -39,12 +54,16 @@ describe('MySomethingList.vue', () => {
|
|||||||
'list-item': '<div class="list-item"></div>',
|
'list-item': '<div class="list-item"></div>',
|
||||||
'edit-item': '<div class="edit-item"></div>',
|
'edit-item': '<div class="edit-item"></div>',
|
||||||
}
|
}
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
mutations,
|
||||||
|
})
|
||||||
return mount(MySomethingList, {
|
return mount(MySomethingList, {
|
||||||
propsData,
|
propsData,
|
||||||
data,
|
data,
|
||||||
mocks,
|
mocks,
|
||||||
localVue,
|
localVue,
|
||||||
slots,
|
slots,
|
||||||
|
store,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,13 +133,42 @@ describe('MySomethingList.vue', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls delete', async () => {
|
it('calls delete by committing "modal/SET_OPEN"', async () => {
|
||||||
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
||||||
deleteButton.trigger('click')
|
deleteButton.trigger('click')
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
|
const expectedModalData = expect.objectContaining({
|
||||||
expect(propsData.callbacks.delete).toHaveBeenCalledTimes(1)
|
name: 'confirm',
|
||||||
expect(propsData.callbacks.delete).toHaveBeenCalledWith(expect.any(Object), expectedItem)
|
data: {
|
||||||
|
type: '',
|
||||||
|
resource: { id: '' },
|
||||||
|
modalData: {
|
||||||
|
titleIdent: 'delete-modal.title',
|
||||||
|
messageIdent: 'delete-modal.message',
|
||||||
|
messageParams: {
|
||||||
|
name: 'dummy',
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
danger: true,
|
||||||
|
icon: 'trash',
|
||||||
|
textIdent: 'delete-modal.confirm-button',
|
||||||
|
callback: expect.any(Function),
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
icon: 'close',
|
||||||
|
textIdent: 'actions.cancel',
|
||||||
|
callback: expect.any(Function),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
expectedModalData,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,11 +9,7 @@
|
|||||||
<div v-if="isEditing">
|
<div v-if="isEditing">
|
||||||
<ds-space margin="base">
|
<ds-space margin="base">
|
||||||
<ds-heading tag="h5">
|
<ds-heading tag="h5">
|
||||||
{{
|
{{ isCreation ? texts.addNew : texts.edit + ' — ' + editingItem[namePropertyKey] }}
|
||||||
isCreation
|
|
||||||
? $t('settings.social-media.addNewTitle')
|
|
||||||
: $t('settings.social-media.editTitle', { name: editingItem[namePropertyKey] })
|
|
||||||
}}
|
|
||||||
</ds-heading>
|
</ds-heading>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
<ds-space v-if="items" margin-top="base">
|
<ds-space v-if="items" margin-top="base">
|
||||||
@ -36,11 +32,11 @@
|
|||||||
data-test="edit-button"
|
data-test="edit-button"
|
||||||
/>
|
/>
|
||||||
<base-button
|
<base-button
|
||||||
|
:title="$t('actions.delete')"
|
||||||
icon="trash"
|
icon="trash"
|
||||||
circle
|
circle
|
||||||
ghost
|
ghost
|
||||||
@click="handleDeleteItem(item)"
|
@click="handleDeleteItem(item)"
|
||||||
:title="$t('actions.delete')"
|
|
||||||
data-test="delete-button"
|
data-test="delete-button"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -58,7 +54,7 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
data-test="add-save-button"
|
data-test="add-save-button"
|
||||||
>
|
>
|
||||||
{{ isEditing ? $t('actions.save') : $t('settings.social-media.submit') }}
|
{{ isEditing ? $t('actions.save') : texts.addButton }}
|
||||||
</base-button>
|
</base-button>
|
||||||
<base-button v-if="isEditing" id="cancel" danger @click="handleCancel()">
|
<base-button v-if="isEditing" id="cancel" danger @click="handleCancel()">
|
||||||
{{ $t('actions.cancel') }}
|
{{ $t('actions.cancel') }}
|
||||||
@ -69,27 +65,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MySomethingList',
|
name: 'MySomethingList',
|
||||||
props: {
|
props: {
|
||||||
useFormData: {
|
useFormData: { type: Object, default: () => ({}) },
|
||||||
|
useFormSchema: { type: Object, default: () => ({}) },
|
||||||
|
useItems: { type: Array, default: () => [] },
|
||||||
|
defaultItem: { type: Object, default: () => ({}) },
|
||||||
|
namePropertyKey: { type: String, required: true },
|
||||||
|
texts: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
useFormSchema: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
useItems: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
defaultItem: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
namePropertyKey: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
@ -128,6 +115,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
commitModalData: 'modal/SET_OPEN',
|
||||||
|
}),
|
||||||
handleInput(data) {
|
handleInput(data) {
|
||||||
this.callbacks.handleInput(this, data)
|
this.callbacks.handleInput(this, data)
|
||||||
this.disabled = true
|
this.disabled = true
|
||||||
@ -155,8 +145,42 @@ export default {
|
|||||||
this.editingItem = null
|
this.editingItem = null
|
||||||
this.disabled = true
|
this.disabled = true
|
||||||
},
|
},
|
||||||
async handleDeleteItem(item) {
|
handleDeleteItem(item) {
|
||||||
await this.callbacks.delete(this, item)
|
this.openModal(item)
|
||||||
|
},
|
||||||
|
openModal(item) {
|
||||||
|
this.commitModalData(this.modalData(item))
|
||||||
|
},
|
||||||
|
modalData(item) {
|
||||||
|
return {
|
||||||
|
name: 'confirm',
|
||||||
|
data: {
|
||||||
|
type: '',
|
||||||
|
resource: { id: '' },
|
||||||
|
modalData: {
|
||||||
|
titleIdent: this.texts.deleteModal.titleIdent,
|
||||||
|
messageIdent: this.texts.deleteModal.messageIdent,
|
||||||
|
messageParams: {
|
||||||
|
name: item[this.namePropertyKey],
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
danger: true,
|
||||||
|
icon: this.texts.deleteModal.confirm.icon,
|
||||||
|
textIdent: this.texts.deleteModal.confirm.buttonTextIdent,
|
||||||
|
callback: () => {
|
||||||
|
this.callbacks.delete(this, item)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
icon: 'close',
|
||||||
|
textIdent: 'actions.cancel',
|
||||||
|
callback: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,7 @@ export default {
|
|||||||
border-radius: $border-radius-x-large;
|
border-radius: $border-radius-x-large;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
|
letter-spacing: $letter-spacing-large;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.--danger {
|
&.--danger {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<span v-if="svgIcon" class="base-icon">
|
<span v-if="svgIcon" class="base-icon">
|
||||||
<component :is="svgIcon" aria-hidden="true" focusable="false" class="svg" />
|
<component :class="['svg', `--${size}`]" :is="svgIcon" aria-hidden="true" focusable="false" />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -16,6 +16,13 @@ export default {
|
|||||||
return iconNames.includes(value)
|
return iconNames.includes(value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'regular',
|
||||||
|
validator(value) {
|
||||||
|
return value.match(/^(small|regular|large)$/)
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
svgIcon() {
|
svgIcon() {
|
||||||
@ -42,6 +49,19 @@ export default {
|
|||||||
> .svg {
|
> .svg {
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
|
||||||
|
&.--small {
|
||||||
|
height: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--regular {
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--large {
|
||||||
|
margin-left: 4px;
|
||||||
|
height: 2.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -28,6 +28,7 @@ const sentry = {
|
|||||||
const options = {
|
const options = {
|
||||||
VERSION: process.env.VERSION || pkg.version,
|
VERSION: process.env.VERSION || pkg.version,
|
||||||
DESCRIPTION: process.env.DESCRIPTION || pkg.description,
|
DESCRIPTION: process.env.DESCRIPTION || pkg.description,
|
||||||
|
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
|
||||||
PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true' || false,
|
PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true' || false,
|
||||||
INVITE_REGISTRATION: process.env.INVITE_REGISTRATION !== 'false', // default = true
|
INVITE_REGISTRATION: process.env.INVITE_REGISTRATION !== 'false', // default = true
|
||||||
// Cookies
|
// Cookies
|
||||||
|
|||||||
@ -13,11 +13,19 @@ export const userFragment = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const locationAndBadgesFragment = (lang) => gql`
|
export const locationFragment = (lang) => gql`
|
||||||
fragment locationAndBadges on User {
|
fragment location on User {
|
||||||
|
locationName
|
||||||
location {
|
location {
|
||||||
name: name${lang}
|
name: name${lang}
|
||||||
|
lng
|
||||||
|
lat
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const badgesFragment = gql`
|
||||||
|
fragment badges on User {
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
icon
|
icon
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import {
|
|||||||
commentFragment,
|
commentFragment,
|
||||||
postCountsFragment,
|
postCountsFragment,
|
||||||
userCountsFragment,
|
userCountsFragment,
|
||||||
locationAndBadgesFragment,
|
locationFragment,
|
||||||
|
badgesFragment,
|
||||||
tagsCategoriesAndPinnedFragment,
|
tagsCategoriesAndPinnedFragment,
|
||||||
} from './Fragments'
|
} from './Fragments'
|
||||||
|
|
||||||
@ -14,7 +15,8 @@ export default (i18n) => {
|
|||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${userCountsFragment}
|
${userCountsFragment}
|
||||||
${locationAndBadgesFragment(lang)}
|
${locationFragment(lang)}
|
||||||
|
${badgesFragment}
|
||||||
${postFragment}
|
${postFragment}
|
||||||
${postCountsFragment}
|
${postCountsFragment}
|
||||||
${tagsCategoriesAndPinnedFragment}
|
${tagsCategoriesAndPinnedFragment}
|
||||||
@ -28,7 +30,8 @@ export default (i18n) => {
|
|||||||
author {
|
author {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
blocked
|
blocked
|
||||||
}
|
}
|
||||||
comments(orderBy: createdAt_asc) {
|
comments(orderBy: createdAt_asc) {
|
||||||
@ -36,7 +39,8 @@ export default (i18n) => {
|
|||||||
author {
|
author {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
group {
|
group {
|
||||||
@ -54,7 +58,8 @@ export const filterPosts = (i18n) => {
|
|||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${userCountsFragment}
|
${userCountsFragment}
|
||||||
${locationAndBadgesFragment(lang)}
|
${locationFragment(lang)}
|
||||||
|
${badgesFragment}
|
||||||
${postFragment}
|
${postFragment}
|
||||||
${postCountsFragment}
|
${postCountsFragment}
|
||||||
${tagsCategoriesAndPinnedFragment}
|
${tagsCategoriesAndPinnedFragment}
|
||||||
@ -67,7 +72,8 @@ export const filterPosts = (i18n) => {
|
|||||||
author {
|
author {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
}
|
}
|
||||||
group {
|
group {
|
||||||
id
|
id
|
||||||
@ -84,7 +90,8 @@ export const profilePagePosts = (i18n) => {
|
|||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${userCountsFragment}
|
${userCountsFragment}
|
||||||
${locationAndBadgesFragment(lang)}
|
${locationFragment(lang)}
|
||||||
|
${badgesFragment}
|
||||||
${postFragment}
|
${postFragment}
|
||||||
${postCountsFragment}
|
${postCountsFragment}
|
||||||
${tagsCategoriesAndPinnedFragment}
|
${tagsCategoriesAndPinnedFragment}
|
||||||
@ -102,7 +109,8 @@ export const profilePagePosts = (i18n) => {
|
|||||||
author {
|
author {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
}
|
}
|
||||||
group {
|
group {
|
||||||
id
|
id
|
||||||
@ -127,7 +135,8 @@ export const relatedContributions = (i18n) => {
|
|||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${userCountsFragment}
|
${userCountsFragment}
|
||||||
${locationAndBadgesFragment(lang)}
|
${locationFragment(lang)}
|
||||||
|
${badgesFragment}
|
||||||
${postFragment}
|
${postFragment}
|
||||||
${postCountsFragment}
|
${postCountsFragment}
|
||||||
${tagsCategoriesAndPinnedFragment}
|
${tagsCategoriesAndPinnedFragment}
|
||||||
@ -140,7 +149,8 @@ export const relatedContributions = (i18n) => {
|
|||||||
author {
|
author {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
}
|
}
|
||||||
relatedContributions(first: 2) {
|
relatedContributions(first: 2) {
|
||||||
...post
|
...post
|
||||||
@ -149,7 +159,8 @@ export const relatedContributions = (i18n) => {
|
|||||||
author {
|
author {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
webapp/graphql/SocialMedia.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
// ------ mutations
|
||||||
|
|
||||||
|
export const createSocialMediaMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($url: String!) {
|
||||||
|
CreateSocialMedia(url: $url) {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSocialMediaMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($id: ID!, $url: String!) {
|
||||||
|
UpdateSocialMedia(id: $id, url: $url) {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSocialMediaMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($id: ID!) {
|
||||||
|
DeleteSocialMedia(id: $id) {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------ queries
|
||||||
|
|
||||||
|
// put the queries here
|
||||||
@ -1,26 +1,28 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import {
|
import {
|
||||||
userCountsFragment,
|
userCountsFragment,
|
||||||
locationAndBadgesFragment,
|
locationFragment,
|
||||||
|
badgesFragment,
|
||||||
userFragment,
|
userFragment,
|
||||||
postFragment,
|
postFragment,
|
||||||
commentFragment,
|
commentFragment,
|
||||||
} from './Fragments'
|
} from './Fragments'
|
||||||
|
|
||||||
export default (i18n) => {
|
export const profileUserQuery = (i18n) => {
|
||||||
const lang = i18n.locale().toUpperCase()
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${userCountsFragment}
|
${userCountsFragment}
|
||||||
${locationAndBadgesFragment(lang)}
|
${locationFragment(lang)}
|
||||||
|
${badgesFragment}
|
||||||
|
|
||||||
query User($id: ID!, $followedByCount: Int, $followingCount: Int) {
|
query User($id: ID!, $followedByCount: Int!, $followingCount: Int!) {
|
||||||
User(id: $id) {
|
User(id: $id) {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
about
|
about
|
||||||
locationName
|
|
||||||
createdAt
|
createdAt
|
||||||
followedByCurrentUser
|
followedByCurrentUser
|
||||||
isMuted
|
isMuted
|
||||||
@ -29,12 +31,14 @@ export default (i18n) => {
|
|||||||
following(first: $followingCount) {
|
following(first: $followingCount) {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
}
|
}
|
||||||
followedBy(first: $followedByCount) {
|
followedBy(first: $followedByCount) {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...location
|
||||||
|
...badges
|
||||||
}
|
}
|
||||||
socialMedia {
|
socialMedia {
|
||||||
id
|
id
|
||||||
@ -62,6 +66,48 @@ export const minimisedUserQuery = () => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const adminUserQuery = () => {
|
||||||
|
return gql`
|
||||||
|
query ($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
|
||||||
|
User(
|
||||||
|
email: $email
|
||||||
|
filter: $filter
|
||||||
|
first: $first
|
||||||
|
offset: $offset
|
||||||
|
orderBy: createdAt_desc
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
email
|
||||||
|
role
|
||||||
|
createdAt
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
|
shoutedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapUserQuery = (i18n) => {
|
||||||
|
const lang = i18n.locale().toUpperCase()
|
||||||
|
return gql`
|
||||||
|
${userFragment}
|
||||||
|
${locationFragment(lang)}
|
||||||
|
${badgesFragment}
|
||||||
|
|
||||||
|
query {
|
||||||
|
User {
|
||||||
|
...user
|
||||||
|
about
|
||||||
|
...location
|
||||||
|
...badges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export const notificationQuery = (i18n) => {
|
export const notificationQuery = (i18n) => {
|
||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
// import { locationFragment } from './Fragments'
|
||||||
|
|
||||||
// ------ mutations
|
// ------ mutations
|
||||||
|
|
||||||
@ -146,7 +147,9 @@ export const changeGroupMemberRoleMutation = () => {
|
|||||||
|
|
||||||
export const groupQuery = (i18n) => {
|
export const groupQuery = (i18n) => {
|
||||||
const lang = i18n ? i18n.locale().toUpperCase() : 'EN'
|
const lang = i18n ? i18n.locale().toUpperCase() : 'EN'
|
||||||
|
// ${locationFragment(lang)}
|
||||||
return gql`
|
return gql`
|
||||||
|
|
||||||
query ($isMember: Boolean, $id: ID, $slug: String, $first: Int, $offset: Int) {
|
query ($isMember: Boolean, $id: ID, $slug: String, $first: Int, $offset: Int) {
|
||||||
Group(isMember: $isMember, id: $id, slug: $slug, first: $first, offset: $offset) {
|
Group(isMember: $isMember, id: $id, slug: $slug, first: $first, offset: $offset) {
|
||||||
id
|
id
|
||||||
@ -171,8 +174,11 @@ export const groupQuery = (i18n) => {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
locationName
|
locationName
|
||||||
|
# ...location
|
||||||
location {
|
location {
|
||||||
name: name${lang}
|
name: name${lang}
|
||||||
|
lng
|
||||||
|
lat
|
||||||
}
|
}
|
||||||
myRole
|
myRole
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-default">
|
<div class="layout-default">
|
||||||
<div class="main-navigation">
|
<div class="main-navigation">
|
||||||
<header-menu :showMobileMenu="showMobileMenu" />
|
<header-menu :showMobileMenu="isMobile" />
|
||||||
</div>
|
</div>
|
||||||
<ds-container>
|
<ds-container>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<nuxt :showMobileMenu="showMobileMenu" />
|
<nuxt :showMobileMenu="showMobileMenu" />
|
||||||
</div>
|
</div>
|
||||||
</ds-container>
|
</ds-container>
|
||||||
<page-footer v-if="!showMobileMenu" />
|
<page-footer v-if="!isMobile" />
|
||||||
<div id="overlay" />
|
<div id="overlay" />
|
||||||
<client-only>
|
<client-only>
|
||||||
<modal />
|
<modal />
|
||||||
@ -17,8 +17,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
|
|
||||||
import seo from '~/mixins/seo'
|
import seo from '~/mixins/seo'
|
||||||
|
import mobile from '~/mixins/mobile'
|
||||||
|
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
import PageFooter from '~/components/PageFooter/PageFooter'
|
import PageFooter from '~/components/PageFooter/PageFooter'
|
||||||
|
|
||||||
@ -28,27 +29,10 @@ export default {
|
|||||||
Modal,
|
Modal,
|
||||||
PageFooter,
|
PageFooter,
|
||||||
},
|
},
|
||||||
mixins: [seo],
|
mixins: [seo, mobile()],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
windowWidth: null,
|
|
||||||
maxMobileWidth: 810,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
showMobileMenu() {
|
|
||||||
if (!this.windowWidth) return false
|
|
||||||
return this.windowWidth <= this.maxMobileWidth
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.windowWidth = window.innerWidth
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
this.windowWidth = window.innerWidth
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.main-navigation {
|
.main-navigation {
|
||||||
background-color: $color-header-background;
|
background-color: $color-header-background;
|
||||||
|
|||||||
@ -402,20 +402,24 @@
|
|||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"actionRadii": {
|
"actionRadii": {
|
||||||
"continental": "Kontinentale Gruppe",
|
"continental": "Kontinental",
|
||||||
"global": "Globale Gruppe",
|
"global": "Global oder nur virtuell",
|
||||||
"interplanetary": "Interplanetare Gruppe",
|
"interplanetary": "Interplanetar",
|
||||||
"national": "Nationale Gruppe",
|
"national": "National",
|
||||||
"regional": "Regionale Gruppe"
|
"regional": "Regional"
|
||||||
},
|
},
|
||||||
"actionRadius": "Aktionsradius",
|
"actionRadius": "Aktionsradius der Gruppe",
|
||||||
"addMemberToGroup": "Zur Gruppe hinzufügen",
|
"addMemberToGroup": "Zur Gruppe hinzufügen",
|
||||||
"addMemberToGroupSuccess": "„{name}“ wurde der Gruppe mit der Rolle „{role}“ hinzugefügt!",
|
"addMemberToGroupSuccess": "„{name}“ wurde der Gruppe mit der Rolle „{role}“ hinzugefügt!",
|
||||||
"addUser": "Benutzer hinzufügen",
|
"addUser": "Benutzer hinzufügen",
|
||||||
"addUserNoOptions": "Keine Nutzer gefunden!",
|
"addUserNoOptions": "Keine Nutzer gefunden!",
|
||||||
"addUserPlaceholder": "Benutzername",
|
"addUserPlaceholder": "Benutzername",
|
||||||
"allGroups": "Alle Gruppen",
|
"allGroups": "Alle Gruppen",
|
||||||
|
"button": {
|
||||||
|
"tooltip": "Gruppen anzeigen"
|
||||||
|
},
|
||||||
"categories": "Thema ::: Themen",
|
"categories": "Thema ::: Themen",
|
||||||
|
"categoriesTitle": "Themen der Gruppe",
|
||||||
"changeMemberRole": "Die Rolle wurde auf „{role}“ geändert!",
|
"changeMemberRole": "Die Rolle wurde auf „{role}“ geändert!",
|
||||||
"contentMenu": {
|
"contentMenu": {
|
||||||
"visitGroupPage": "Gruppe anzeigen"
|
"visitGroupPage": "Gruppe anzeigen"
|
||||||
@ -477,11 +481,16 @@
|
|||||||
"usual": "Einfaches Mitglied"
|
"usual": "Einfaches Mitglied"
|
||||||
},
|
},
|
||||||
"save": "Neue Gruppe anlegen",
|
"save": "Neue Gruppe anlegen",
|
||||||
"type": "Gruppentyp",
|
"type": "Öffentlichkeit der Gruppe",
|
||||||
"types": {
|
"types": {
|
||||||
"closed": "Geschlossene Gruppe",
|
"closed": "Geschlossen",
|
||||||
"hidden": "Versteckte Gruppe",
|
"hidden": "Geheim",
|
||||||
"public": "Öffentliche Gruppe"
|
"public": "Öffentlich"
|
||||||
|
},
|
||||||
|
"typesOptions": {
|
||||||
|
"closed": "Geschlossen — Alle Beiträge nur für Gruppenmitglieder sichtbar",
|
||||||
|
"hidden": "Geheim — Gruppe (auch der Name) komplett unsichtbar",
|
||||||
|
"public": "Öffentlich — Gruppe und alle Beiträge für registrierte Nutzer sichtbar"
|
||||||
},
|
},
|
||||||
"update": "Änderung speichern",
|
"update": "Änderung speichern",
|
||||||
"updatedGroup": "Die Gruppendaten wurden geändert!"
|
"updatedGroup": "Die Gruppendaten wurden geändert!"
|
||||||
@ -527,6 +536,24 @@
|
|||||||
"questions": "Bei Fragen oder Problemen erreichst Du uns per E-Mail an",
|
"questions": "Bei Fragen oder Problemen erreichst Du uns per E-Mail an",
|
||||||
"title": "{APPLICATION_NAME} befindet sich in der Wartung"
|
"title": "{APPLICATION_NAME} befindet sich in der Wartung"
|
||||||
},
|
},
|
||||||
|
"map": {
|
||||||
|
"alertMessage": "Es kann nicht auf die Karte zugegriffen werden: Der Mapbox-Token ist auf dem Server nicht gesetzt!",
|
||||||
|
"button": {
|
||||||
|
"tooltip": "Landkarte anzeigen"
|
||||||
|
},
|
||||||
|
"markerTypes": {
|
||||||
|
"group": "Gruppe",
|
||||||
|
"theUser": "deine Position",
|
||||||
|
"user": "Benutzer"
|
||||||
|
},
|
||||||
|
"pageTitle": "Landkarte",
|
||||||
|
"styles": {
|
||||||
|
"dark": "Dunkel",
|
||||||
|
"outdoors": "Landschaft",
|
||||||
|
"satellite": "Satellit",
|
||||||
|
"streets": "Straßen"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"created": "Erstellt"
|
"created": "Erstellt"
|
||||||
@ -800,6 +827,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"labelBio": "Über Dich",
|
"labelBio": "Über Dich",
|
||||||
"labelCity": "Deine Stadt oder Region",
|
"labelCity": "Deine Stadt oder Region",
|
||||||
|
"labelCityHint": "(zeigt ungefähre Position auf der Landkarte)",
|
||||||
"labelName": "Dein Name",
|
"labelName": "Dein Name",
|
||||||
"labelSlug": "Dein eindeutiger Benutzername",
|
"labelSlug": "Dein eindeutiger Benutzername",
|
||||||
"name": "Deine Daten",
|
"name": "Deine Daten",
|
||||||
@ -920,9 +948,14 @@
|
|||||||
"name": "Sicherheit"
|
"name": "Sicherheit"
|
||||||
},
|
},
|
||||||
"social-media": {
|
"social-media": {
|
||||||
"addNewTitle": "Neuen Link hinzufügen",
|
"add-new-link": "Neuen Link hinzufügen",
|
||||||
"editTitle": "Link \"{name}\" ändern",
|
"delete-modal": {
|
||||||
"name": "Soziale Netzwerke",
|
"confirm-button": "Löschen",
|
||||||
|
"message": "Lösche „{name}“.",
|
||||||
|
"title": "Möchtest du wirklich deinen Link löschen?"
|
||||||
|
},
|
||||||
|
"edit-link": "Ändere den Link",
|
||||||
|
"name": "Soziale Medien",
|
||||||
"placeholder": "Deine Webadresse des Sozialen Netzwerkes",
|
"placeholder": "Deine Webadresse des Sozialen Netzwerkes",
|
||||||
"requireUnique": "Dieser Link existiert bereits",
|
"requireUnique": "Dieser Link existiert bereits",
|
||||||
"submit": "Link hinzufügen",
|
"submit": "Link hinzufügen",
|
||||||
|
|||||||
@ -402,20 +402,24 @@
|
|||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"actionRadii": {
|
"actionRadii": {
|
||||||
"continental": "Continental Group",
|
"continental": "Continental",
|
||||||
"global": "Global Group",
|
"global": "Global or only virtual",
|
||||||
"interplanetary": "Interplanetary Group",
|
"interplanetary": "Interplanetary",
|
||||||
"national": "National Group",
|
"national": "National",
|
||||||
"regional": "Regional Group"
|
"regional": "Regional"
|
||||||
},
|
},
|
||||||
"actionRadius": "Action radius",
|
"actionRadius": "Action radius of the group",
|
||||||
"addMemberToGroup": "Add to group",
|
"addMemberToGroup": "Add to group",
|
||||||
"addMemberToGroupSuccess": "“{name}” was added to the group with the role “{role}”!",
|
"addMemberToGroupSuccess": "“{name}” was added to the group with the role “{role}”!",
|
||||||
"addUser": "Add User",
|
"addUser": "Add User",
|
||||||
"addUserNoOptions": "No users found!",
|
"addUserNoOptions": "No users found!",
|
||||||
"addUserPlaceholder": " Username",
|
"addUserPlaceholder": " Username",
|
||||||
"allGroups": "All Groups",
|
"allGroups": "All Groups",
|
||||||
|
"button": {
|
||||||
|
"tooltip": "Show groups"
|
||||||
|
},
|
||||||
"categories": "Topic ::: Topics",
|
"categories": "Topic ::: Topics",
|
||||||
|
"categoriesTitle": "Topics of the group",
|
||||||
"changeMemberRole": "The role has been changed to “{role}”!",
|
"changeMemberRole": "The role has been changed to “{role}”!",
|
||||||
"contentMenu": {
|
"contentMenu": {
|
||||||
"visitGroupPage": "Show group"
|
"visitGroupPage": "Show group"
|
||||||
@ -477,11 +481,16 @@
|
|||||||
"usual": "Simple Member"
|
"usual": "Simple Member"
|
||||||
},
|
},
|
||||||
"save": "Create new group",
|
"save": "Create new group",
|
||||||
"type": "Group type",
|
"type": "Visibility of the group",
|
||||||
"types": {
|
"types": {
|
||||||
"closed": "Closed Group",
|
"closed": "Closed",
|
||||||
"hidden": "Hidden Group",
|
"hidden": "Secret",
|
||||||
"public": "Public Group"
|
"public": "Public"
|
||||||
|
},
|
||||||
|
"typesOptions": {
|
||||||
|
"closed": "Closed — All posts only visible to the group's members",
|
||||||
|
"hidden": "Secret — Group (including the name) is completely invisible",
|
||||||
|
"public": "Public — Group and all posts are visible for all registered users"
|
||||||
},
|
},
|
||||||
"update": "Save change",
|
"update": "Save change",
|
||||||
"updatedGroup": "The group data has been changed."
|
"updatedGroup": "The group data has been changed."
|
||||||
@ -527,6 +536,24 @@
|
|||||||
"questions": "Any Questions or concerns, send an e-mail to",
|
"questions": "Any Questions or concerns, send an e-mail to",
|
||||||
"title": "{APPLICATION_NAME} is under maintenance"
|
"title": "{APPLICATION_NAME} is under maintenance"
|
||||||
},
|
},
|
||||||
|
"map": {
|
||||||
|
"alertMessage": "The map cannot be accessed: The Mapbox token is not set on the server!",
|
||||||
|
"button": {
|
||||||
|
"tooltip": "Show map"
|
||||||
|
},
|
||||||
|
"markerTypes": {
|
||||||
|
"group": "group",
|
||||||
|
"theUser": "your position",
|
||||||
|
"user": "user"
|
||||||
|
},
|
||||||
|
"pageTitle": "Map",
|
||||||
|
"styles": {
|
||||||
|
"dark": "Dark",
|
||||||
|
"outdoors": "Outdoors",
|
||||||
|
"satellite": "Satellite",
|
||||||
|
"streets": "Streets"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"created": "Created"
|
"created": "Created"
|
||||||
@ -800,6 +827,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"labelBio": "About You",
|
"labelBio": "About You",
|
||||||
"labelCity": "Your City or Region",
|
"labelCity": "Your City or Region",
|
||||||
|
"labelCityHint": "(shows approximate position on map)",
|
||||||
"labelName": "Your Name",
|
"labelName": "Your Name",
|
||||||
"labelSlug": "Your unique user name",
|
"labelSlug": "Your unique user name",
|
||||||
"name": "Your data",
|
"name": "Your data",
|
||||||
@ -920,8 +948,13 @@
|
|||||||
"name": "Security"
|
"name": "Security"
|
||||||
},
|
},
|
||||||
"social-media": {
|
"social-media": {
|
||||||
"addNewTitle": "Add new link",
|
"add-new-link": "Add new link",
|
||||||
"editTitle": "Edit link \"{name}\"",
|
"delete-modal": {
|
||||||
|
"confirm-button": "Delete",
|
||||||
|
"message": "Delete “{name}”.",
|
||||||
|
"title": "Do you really want to delete your link?"
|
||||||
|
},
|
||||||
|
"edit-link": "Edit link",
|
||||||
"name": "Social media",
|
"name": "Social media",
|
||||||
"placeholder": "Your social media url",
|
"placeholder": "Your social media url",
|
||||||
"requireUnique": "You added this url already",
|
"requireUnique": "You added this url already",
|
||||||
|
|||||||
@ -8,9 +8,9 @@
|
|||||||
"private": false,
|
"private": false,
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "yarn run nuxt -c nuxt.config.maintenance.js",
|
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt -c nuxt.config.maintenance.js",
|
||||||
"build": "yarn run nuxt build -c nuxt.config.maintenance.js",
|
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt build -c nuxt.config.maintenance.js",
|
||||||
"start": "yarn run nuxt start -c nuxt.config.maintenance.js",
|
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt start -c nuxt.config.maintenance.js",
|
||||||
"generate": "yarn run nuxt generate -c nuxt.config.maintenance.js"
|
"generate": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt generate -c nuxt.config.maintenance.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
webapp/mixins/mobile.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export default (mobileWidth = null) => {
|
||||||
|
return {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
windowWidth: null,
|
||||||
|
maxMobileWidth: mobileWidth || 810, // greater counts as desktop
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMobile() {
|
||||||
|
if (!this.windowWidth) return false
|
||||||
|
return this.windowWidth <= this.maxMobileWidth
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -126,6 +126,7 @@ export default {
|
|||||||
{ src: '~/plugins/vue-filters.js' },
|
{ src: '~/plugins/vue-filters.js' },
|
||||||
{ src: '~/plugins/vue-infinite-loading.js', ssr: false },
|
{ src: '~/plugins/vue-infinite-loading.js', ssr: false },
|
||||||
{ src: '~/plugins/vue-observe-visibility.js', ssr: false },
|
{ src: '~/plugins/vue-observe-visibility.js', ssr: false },
|
||||||
|
{ src: '~/plugins/v-mapbox.js', mode: 'client' },
|
||||||
],
|
],
|
||||||
|
|
||||||
router: {
|
router: {
|
||||||
@ -155,6 +156,11 @@ export default {
|
|||||||
'@nuxtjs/pwa',
|
'@nuxtjs/pwa',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
buildModules: [
|
||||||
|
// https://composition-api.nuxtjs.org/getting-started/setup#quick-start
|
||||||
|
'@nuxtjs/composition-api/module',
|
||||||
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Axios module configuration
|
** Axios module configuration
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -7,13 +7,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt",
|
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt",
|
||||||
"dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn run dev",
|
"dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn run dev",
|
||||||
"storybook": "start-storybook -p 3002 -s ./static -c storybook/",
|
"storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 3002 -s ./static -c storybook/",
|
||||||
"build": "nuxt build",
|
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt build",
|
||||||
"start": "nuxt start",
|
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt start",
|
||||||
"generate:maintenance": "nuxt generate -c nuxt.config.maintenance.js",
|
"generate": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt generate",
|
||||||
"generate": "nuxt generate",
|
|
||||||
"lint": "eslint --ext .js,.vue .",
|
"lint": "eslint --ext .js,.vue .",
|
||||||
"locales": "../scripts/translations/missing-keys.sh && ../scripts/translations/sort.sh",
|
"locales": "../scripts/translations/missing-keys.sh && ../scripts/translations/sort.sh",
|
||||||
"precommit": "yarn lint",
|
"precommit": "yarn lint",
|
||||||
@ -22,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@human-connection/styleguide": "0.5.22",
|
"@human-connection/styleguide": "0.5.22",
|
||||||
|
"@mapbox/mapbox-gl-geocoder": "^5.0.1",
|
||||||
"@nuxtjs/apollo": "^4.0.0-rc19",
|
"@nuxtjs/apollo": "^4.0.0-rc19",
|
||||||
"@nuxtjs/axios": "~5.9.7",
|
"@nuxtjs/axios": "~5.9.7",
|
||||||
"@nuxtjs/dotenv": "~1.4.1",
|
"@nuxtjs/dotenv": "~1.4.1",
|
||||||
@ -40,6 +40,7 @@
|
|||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"linkify-it": "~3.0.2",
|
"linkify-it": "~3.0.2",
|
||||||
|
"mapbox-gl": "1.13.2",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"nuxt": "~2.12.1",
|
"nuxt": "~2.12.1",
|
||||||
"nuxt-dropzone": "^1.0.4",
|
"nuxt-dropzone": "^1.0.4",
|
||||||
@ -50,6 +51,7 @@
|
|||||||
"tiptap": "~1.26.6",
|
"tiptap": "~1.26.6",
|
||||||
"tiptap-extensions": "~1.28.8",
|
"tiptap-extensions": "~1.28.8",
|
||||||
"trunc-html": "^1.1.2",
|
"trunc-html": "^1.1.2",
|
||||||
|
"v-mapbox": "^1.11.2",
|
||||||
"v-tooltip": "~2.1.3",
|
"v-tooltip": "~2.1.3",
|
||||||
"validator": "^13.0.0",
|
"validator": "^13.0.0",
|
||||||
"vue-count-to": "~1.0.13",
|
"vue-count-to": "~1.0.13",
|
||||||
@ -67,6 +69,7 @@
|
|||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/preset-env": "~7.9.0",
|
"@babel/preset-env": "~7.9.0",
|
||||||
"@faker-js/faker": "5.1.0",
|
"@faker-js/faker": "5.1.0",
|
||||||
|
"@nuxtjs/composition-api": "0.32.0",
|
||||||
"@storybook/addon-a11y": "^6.3.6",
|
"@storybook/addon-a11y": "^6.3.6",
|
||||||
"@storybook/addon-actions": "^5.3.21",
|
"@storybook/addon-actions": "^5.3.21",
|
||||||
"@storybook/addon-notes": "^5.3.18",
|
"@storybook/addon-notes": "^5.3.18",
|
||||||
@ -108,5 +111,8 @@
|
|||||||
"vue-jest": "~3.0.5",
|
"vue-jest": "~3.0.5",
|
||||||
"vue-svg-loader": "~0.16.0",
|
"vue-svg-loader": "~0.16.0",
|
||||||
"vue-template-compiler": "^2.6.11"
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"nan": "2.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,10 +74,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import { isEmail } from 'validator'
|
import { isEmail } from 'validator'
|
||||||
import normalizeEmail from '~/components/utils/NormalizeEmail'
|
import normalizeEmail from '~/components/utils/NormalizeEmail'
|
||||||
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
||||||
|
import { adminUserQuery } from '~/graphql/User'
|
||||||
import { FetchAllRoles, updateUserRole } from '~/graphql/admin/Roles'
|
import { FetchAllRoles, updateUserRole } from '~/graphql/admin/Roles'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -138,27 +138,7 @@ export default {
|
|||||||
apollo: {
|
apollo: {
|
||||||
User: {
|
User: {
|
||||||
query() {
|
query() {
|
||||||
return gql`
|
return adminUserQuery()
|
||||||
query ($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
|
|
||||||
User(
|
|
||||||
email: $email
|
|
||||||
filter: $filter
|
|
||||||
first: $first
|
|
||||||
offset: $offset
|
|
||||||
orderBy: createdAt_desc
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
email
|
|
||||||
role
|
|
||||||
createdAt
|
|
||||||
contributionsCount
|
|
||||||
commentedCount
|
|
||||||
shoutedCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
const { offset, first, email, filter } = this
|
const { offset, first, email, filter } = this
|
||||||
|
|||||||
164
webapp/pages/map.spec.js
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import mapboxgl from 'mapbox-gl'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import VueMeta from 'vue-meta'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import Map from './map'
|
||||||
|
|
||||||
|
jest.mock('mapbox-gl', () => {
|
||||||
|
return {
|
||||||
|
GeolocateControl: jest.fn(),
|
||||||
|
Map: jest.fn(() => ({
|
||||||
|
addControl: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
})),
|
||||||
|
NavigationControl: jest.fn(),
|
||||||
|
Popup: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
isOpen: jest.fn(),
|
||||||
|
setLngLat: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
setHTML: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
addTo: jest.fn(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
localVue.use(VueMeta, { keyName: 'head' })
|
||||||
|
|
||||||
|
const onEventMocks = {}
|
||||||
|
|
||||||
|
const mapOnMock = jest.fn((key, ...args) => {
|
||||||
|
onEventMocks[key] = args[args.length - 1]
|
||||||
|
})
|
||||||
|
const mapAddControlMock = jest.fn()
|
||||||
|
|
||||||
|
const mapMock = {
|
||||||
|
on: mapOnMock,
|
||||||
|
addControl: mapAddControlMock,
|
||||||
|
loadImage: jest.fn(),
|
||||||
|
getCanvas: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
style: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const stubs = {
|
||||||
|
'client-only': true,
|
||||||
|
'mgl-map': true,
|
||||||
|
MglFullscreenControl: true,
|
||||||
|
MglNavigationControl: true,
|
||||||
|
MglGeolocateControl: true,
|
||||||
|
MglScaleControl: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('map', () => {
|
||||||
|
let wrapper
|
||||||
|
let mocks
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: (t) => t,
|
||||||
|
$env: {
|
||||||
|
MAPBOX_TOKEN: 'MY_MAPBOX_TOKEN',
|
||||||
|
},
|
||||||
|
$toast: {
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({ getters: { 'auth/user': () => false } })
|
||||||
|
return mount(Map, {
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
stubs,
|
||||||
|
store,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(wrapper.is('div')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has correct <head> content', () => {
|
||||||
|
expect(wrapper.vm.$metaInfo.title).toBe('map.pageTitle')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('trigger map load', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.find('mgl-map-stub').vm.$emit('load', { map: mapMock })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes on style load', () => {
|
||||||
|
expect(mapOnMock).toBeCalledWith('style.load', expect.any(Function))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes on mouseenter', () => {
|
||||||
|
expect(mapOnMock).toBeCalledWith('mouseenter', 'markers', expect.any(Function))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes on mouseleave', () => {
|
||||||
|
expect(mapOnMock).toBeCalledWith('mouseleave', 'markers', expect.any(Function))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls add map control', () => {
|
||||||
|
expect(mapAddControlMock).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('trigger style load event', () => {
|
||||||
|
let spy
|
||||||
|
beforeEach(() => {
|
||||||
|
spy = jest.spyOn(wrapper.vm, 'loadMarkersIconsAndAddMarkers')
|
||||||
|
onEventMocks['style.load']()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls loadMarkersIconsAndAddMarkers', () => {
|
||||||
|
expect(spy).toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('trigger mouse enter event', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
onEventMocks.mouseenter({
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
geometry: {
|
||||||
|
coordinates: [100, 200],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lngLat: {
|
||||||
|
lng: 100,
|
||||||
|
lat: 200,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works without errors and warnings', () => {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
483
webapp/pages/map.vue
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
<!-- Example Reference: https://codesandbox.io/s/v-mapbox-with-nuxt-lbrt6?file=/pages/index.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ds-space margin="small">
|
||||||
|
<ds-heading tag="h1">{{ $t('map.pageTitle') }}</ds-heading>
|
||||||
|
</ds-space>
|
||||||
|
<ds-space margin="large" />
|
||||||
|
<client-only v-if="!isEmpty($env.MAPBOX_TOKEN)">
|
||||||
|
<map-styles-buttons
|
||||||
|
v-if="isMobile"
|
||||||
|
:styles="styles"
|
||||||
|
:actualStyle="mapOptions.style"
|
||||||
|
:setStyle="setStyle"
|
||||||
|
/>
|
||||||
|
<mgl-map
|
||||||
|
:mapbox-gl="mapboxgl"
|
||||||
|
:access-token="mapOptions.accessToken"
|
||||||
|
:map-style.sync="mapOptions.style"
|
||||||
|
:center="mapOptions.center"
|
||||||
|
:zoom="mapOptions.zoom"
|
||||||
|
:max-zoom="mapOptions.maxZoom"
|
||||||
|
:cross-source-collisions="false"
|
||||||
|
:fail-if-major-performance-caveat="false"
|
||||||
|
:preserve-drawing-buffer="true"
|
||||||
|
:hash="false"
|
||||||
|
:min-pitch="0"
|
||||||
|
:max-pitch="60"
|
||||||
|
@load="onMapLoad"
|
||||||
|
>
|
||||||
|
<map-styles-buttons
|
||||||
|
v-if="!isMobile"
|
||||||
|
:styles="styles"
|
||||||
|
:actualStyle="mapOptions.style"
|
||||||
|
:setStyle="setStyle"
|
||||||
|
/>
|
||||||
|
<MglFullscreenControl />
|
||||||
|
<MglNavigationControl position="top-right" />
|
||||||
|
<MglGeolocateControl position="top-right" />
|
||||||
|
<MglScaleControl />
|
||||||
|
</mgl-map>
|
||||||
|
</client-only>
|
||||||
|
<empty v-else icon="alert" :message="$t('map.alertMessage')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { isEmpty, toArray } from 'lodash'
|
||||||
|
import mapboxgl from 'mapbox-gl'
|
||||||
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
|
||||||
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
|
||||||
|
import { groupQuery } from '~/graphql/groups'
|
||||||
|
import mobile from '~/mixins/mobile'
|
||||||
|
import Empty from '~/components/Empty/Empty'
|
||||||
|
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
||||||
|
|
||||||
|
const maxMobileWidth = 639 // on this width and smaller the mapbox 'MapboxGeocoder' search gets bigger
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Map',
|
||||||
|
mixins: [mobile(maxMobileWidth)],
|
||||||
|
components: {
|
||||||
|
Empty,
|
||||||
|
MapStylesButtons,
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t('map.pageTitle'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
mapboxgl.accessToken = this.$env.MAPBOX_TOKEN
|
||||||
|
return {
|
||||||
|
isEmpty,
|
||||||
|
mapboxgl,
|
||||||
|
activeStyle: null,
|
||||||
|
defaultCenter: [10.452764, 51.165707], // center of Germany: https://www.gpskoordinaten.de/karte/land/DE
|
||||||
|
currentUserLocation: null,
|
||||||
|
currentUserCoordinates: null,
|
||||||
|
users: null,
|
||||||
|
groups: null,
|
||||||
|
markers: {
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
id: 'marker-blue',
|
||||||
|
name: 'mapbox-marker-icon-20px-blue.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'marker-orange',
|
||||||
|
name: 'mapbox-marker-icon-20px-orange.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'marker-green',
|
||||||
|
name: 'mapbox-marker-icon-20px-green.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isImagesLoaded: false,
|
||||||
|
geoJSON: [],
|
||||||
|
isGeoJSON: false,
|
||||||
|
isSourceAndLayerAdded: false,
|
||||||
|
isFlyToCenter: false,
|
||||||
|
popup: null,
|
||||||
|
popupOnLeaveTimeoutId: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.currentUserLocation = await this.getUserLocation(this.currentUser.id)
|
||||||
|
this.currentUserCoordinates = this.currentUserLocation
|
||||||
|
? this.getCoordinates(this.currentUserLocation)
|
||||||
|
: null
|
||||||
|
this.addMarkersOnCheckPrepared()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentUser: 'auth/user',
|
||||||
|
}),
|
||||||
|
isPreparedForMarkers() {
|
||||||
|
return (
|
||||||
|
!this.markers.isGeoJSON &&
|
||||||
|
this.markers.isImagesLoaded &&
|
||||||
|
this.currentUser &&
|
||||||
|
this.users &&
|
||||||
|
this.groups
|
||||||
|
)
|
||||||
|
},
|
||||||
|
styles() {
|
||||||
|
return toArray(this.availableStyles)
|
||||||
|
},
|
||||||
|
availableStyles() {
|
||||||
|
// https://docs.mapbox.com/api/maps/styles/
|
||||||
|
const availableStyles = {
|
||||||
|
outdoors: {
|
||||||
|
url: 'mapbox://styles/mapbox/outdoors-v12?optimize=true',
|
||||||
|
},
|
||||||
|
streets: {
|
||||||
|
url: 'mapbox://styles/mapbox/streets-v11?optimize=true',
|
||||||
|
// use the newest version?
|
||||||
|
// url: 'mapbox://styles/mapbox/streets-v12',
|
||||||
|
},
|
||||||
|
satellite: {
|
||||||
|
url: 'mapbox://styles/mapbox/satellite-streets-v11?optimize=true',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
url: 'mapbox://styles/mapbox/dark-v10?optimize=true',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Object.keys(availableStyles).map((key) => {
|
||||||
|
availableStyles[key].title = this.$t('map.styles.' + key)
|
||||||
|
})
|
||||||
|
return availableStyles
|
||||||
|
},
|
||||||
|
mapOptions() {
|
||||||
|
return {
|
||||||
|
// accessToken: this.$env.MAPBOX_TOKEN, // is set already above
|
||||||
|
style: !this.activeStyle ? this.availableStyles.outdoors.url : this.activeStyle,
|
||||||
|
center: this.mapCenter,
|
||||||
|
zoom: this.mapZoom,
|
||||||
|
maxZoom: 22,
|
||||||
|
// projection: 'globe', // the package is probably to old, because of Vue2: https://docs.mapbox.com/mapbox-gl-js/example/globe/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mapCenter() {
|
||||||
|
return this.currentUserCoordinates ? this.currentUserCoordinates : this.defaultCenter
|
||||||
|
},
|
||||||
|
mapZoom() {
|
||||||
|
return this.currentUserCoordinates ? 10 : 4
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isPreparedForMarkers(newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.addMarkersOnCheckPrepared()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onMapLoad({ map }) {
|
||||||
|
this.map = map
|
||||||
|
|
||||||
|
// set the default atmosphere style
|
||||||
|
// this.map.setFog({}) // the package is probably to old, because of Vue2: https://docs.mapbox.com/mapbox-gl-js/example/globe/
|
||||||
|
|
||||||
|
this.map.on('style.load', (value) => {
|
||||||
|
// Triggered when `setStyle` is called.
|
||||||
|
this.markers.isImagesLoaded = false
|
||||||
|
this.markers.isSourceAndLayerAdded = false
|
||||||
|
this.loadMarkersIconsAndAddMarkers()
|
||||||
|
})
|
||||||
|
|
||||||
|
// add search field for locations
|
||||||
|
this.map.addControl(
|
||||||
|
new MapboxGeocoder({
|
||||||
|
accessToken: this.$env.MAPBOX_TOKEN,
|
||||||
|
mapboxgl: this.mapboxgl,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// example for popup: https://docs.mapbox.com/mapbox-gl-js/example/popup-on-hover/
|
||||||
|
// create a popup, but don't add it to the map yet
|
||||||
|
this.markers.popup = new mapboxgl.Popup({
|
||||||
|
closeButton: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.map.on('mouseenter', 'markers', (e) => {
|
||||||
|
// if (e.features[0].properties.type !== 'theUser') {}
|
||||||
|
if (this.popupOnLeaveTimeoutId) {
|
||||||
|
clearTimeout(this.popupOnLeaveTimeoutId)
|
||||||
|
this.popupOnLeaveTimeoutId = null
|
||||||
|
}
|
||||||
|
if (this.markers.popup.isOpen()) {
|
||||||
|
this.map.getCanvas().style.cursor = ''
|
||||||
|
this.markers.popup.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the cursor style as a UI indicator.
|
||||||
|
this.map.getCanvas().style.cursor = 'pointer'
|
||||||
|
|
||||||
|
// Copy coordinates array.
|
||||||
|
const coordinates = e.features[0].geometry.coordinates.slice()
|
||||||
|
const markerTypeLabel =
|
||||||
|
e.features[0].properties.type === 'group'
|
||||||
|
? this.$t('map.markerTypes.group')
|
||||||
|
: e.features[0].properties.type === 'user'
|
||||||
|
? this.$t('map.markerTypes.user')
|
||||||
|
: this.$t('map.markerTypes.theUser')
|
||||||
|
const markerProfileLinkTitle =
|
||||||
|
(e.features[0].properties.type === 'group' ? '&' : '@') + e.features[0].properties.slug
|
||||||
|
const markerProfileLink =
|
||||||
|
(e.features[0].properties.type === 'group' ? '/group' : '/profile') +
|
||||||
|
`/${e.features[0].properties.id}/${e.features[0].properties.slug}`
|
||||||
|
let description = `
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<b>${e.features[0].properties.name}</b> <i>(${markerTypeLabel})</i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="${markerProfileLink}" target="_blank">${markerProfileLinkTitle}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
description +=
|
||||||
|
e.features[0].properties.about && e.features[0].properties.about.length > 0
|
||||||
|
? `
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
${e.features[0].properties.about}
|
||||||
|
</div>`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
// Ensure that if the map is zoomed out such that multiple
|
||||||
|
// copies of the feature are visible, the popup appears
|
||||||
|
// over the copy being pointed to.
|
||||||
|
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||||
|
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the popup and set its coordinates
|
||||||
|
// based on the feature found.
|
||||||
|
this.markers.popup.setLngLat(coordinates).setHTML(description).addTo(this.map)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.map.on('mouseleave', 'markers', (e) => {
|
||||||
|
if (this.markers.popup.isOpen()) {
|
||||||
|
this.popupOnLeaveTimeoutId = setTimeout(() => {
|
||||||
|
this.map.getCanvas().style.cursor = ''
|
||||||
|
this.markers.popup.remove()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.loadMarkersIconsAndAddMarkers()
|
||||||
|
},
|
||||||
|
language(map) {
|
||||||
|
// example in mapbox-gl-language: https://github.com/mapbox/mapbox-gl-language/blob/master/index.js
|
||||||
|
map.getStyle().layers.forEach(function (thisLayer) {
|
||||||
|
if (thisLayer.id.indexOf('-label') > 0) {
|
||||||
|
// seems to use user language. specific language would be `name_de`, but is not compatible with all maps
|
||||||
|
// variant sets all 'text-field' layers to languages of their countries
|
||||||
|
map.setLayoutProperty(thisLayer.id, 'text-field', ['get', 'name'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setStyle(url) {
|
||||||
|
this.map.setStyle(url)
|
||||||
|
this.activeStyle = url
|
||||||
|
},
|
||||||
|
loadMarkersIconsAndAddMarkers() {
|
||||||
|
Promise.all(
|
||||||
|
this.markers.icons.map(
|
||||||
|
(marker) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
// our images have to be in the 'static/img/*' folder otherwise they are not reachable via URL
|
||||||
|
this.map.loadImage('img/mapbox/marker-icons/' + marker.name, (error, image) => {
|
||||||
|
if (error) throw error
|
||||||
|
this.map.addImage(marker.id, image)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).then(() => {
|
||||||
|
this.markers.isImagesLoaded = true
|
||||||
|
this.language(this.map)
|
||||||
|
this.addMarkersOnCheckPrepared()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addMarkersOnCheckPrepared() {
|
||||||
|
// set geoJSON for markers
|
||||||
|
if (this.isPreparedForMarkers) {
|
||||||
|
// add markers for "users"
|
||||||
|
this.users.forEach((user) => {
|
||||||
|
if (user.id !== this.currentUser.id && user.location) {
|
||||||
|
this.markers.geoJSON.push({
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {
|
||||||
|
type: 'user',
|
||||||
|
iconName: 'marker-green',
|
||||||
|
iconRotate: 0.0,
|
||||||
|
id: user.id,
|
||||||
|
slug: user.slug,
|
||||||
|
name: user.name,
|
||||||
|
about: user.about,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: this.getCoordinates(user.location),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// add markers for "groups"
|
||||||
|
this.groups.forEach((group) => {
|
||||||
|
if (group.location) {
|
||||||
|
this.markers.geoJSON.push({
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {
|
||||||
|
type: 'group',
|
||||||
|
iconName: 'marker-blue',
|
||||||
|
iconRotate: 0.0,
|
||||||
|
id: group.id,
|
||||||
|
slug: group.slug,
|
||||||
|
name: group.name,
|
||||||
|
about: group.about,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: this.getCoordinates(group.location),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// add marker for "currentUser"
|
||||||
|
if (this.currentUserCoordinates) {
|
||||||
|
this.markers.geoJSON.push({
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {
|
||||||
|
type: 'theUser',
|
||||||
|
iconName: 'marker-orange',
|
||||||
|
iconRotate: 45.0,
|
||||||
|
id: this.currentUser.id,
|
||||||
|
slug: this.currentUser.slug,
|
||||||
|
name: this.currentUser.name,
|
||||||
|
about: this.currentUser.about,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: this.currentUserCoordinates,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.markers.isGeoJSON = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// add source and layer
|
||||||
|
if (!this.markers.isSourceAndLayerAdded && this.markers.isGeoJSON && this.map) {
|
||||||
|
this.map.addSource('markers', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: this.markers.geoJSON,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.map.addLayer({
|
||||||
|
id: 'markers',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'markers',
|
||||||
|
layout: {
|
||||||
|
'icon-image': ['get', 'iconName'], // get the "icon-image" from the source's "iconName" property
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
'icon-size': 1.0,
|
||||||
|
'icon-rotate': ['get', 'iconRotate'], // get the "icon-rotate" from the source's "iconRotate" property
|
||||||
|
// 'text-field': ['get', 'name'], // get the "text-field" from the source's "name" property
|
||||||
|
// 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
|
||||||
|
// 'text-offset': [0, 0],
|
||||||
|
// 'text-anchor': 'top',
|
||||||
|
// 'text-allow-overlap': true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.markers.isSourceAndLayerAdded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// fly to center if never done
|
||||||
|
if (!this.markers.isFlyToCenter && this.markers.isSourceAndLayerAdded) {
|
||||||
|
this.mapFlyToCenter()
|
||||||
|
this.markers.isFlyToCenter = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mapFlyToCenter() {
|
||||||
|
if (this.map) {
|
||||||
|
// example: https://docs.mapbox.com/mapbox-gl-js/example/center-on-feature/
|
||||||
|
this.map.flyTo({
|
||||||
|
center: this.mapCenter,
|
||||||
|
zoom: this.mapZoom,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCoordinates(location) {
|
||||||
|
return [location.lng, location.lat]
|
||||||
|
},
|
||||||
|
async getUserLocation(id) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { User: users },
|
||||||
|
} = await this.$apollo.query({
|
||||||
|
query: profileUserQuery(this.$i18n),
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
followedByCount: 0,
|
||||||
|
followingCount: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return users && users[0] && users[0].location ? users[0].location : null
|
||||||
|
} catch (err) {
|
||||||
|
this.$toast.error(err.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
User: {
|
||||||
|
query() {
|
||||||
|
return mapUserQuery(this.$i18n)
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
update({ User }) {
|
||||||
|
this.users = User
|
||||||
|
this.addMarkersOnCheckPrepared()
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
},
|
||||||
|
Group: {
|
||||||
|
query() {
|
||||||
|
return groupQuery(this.$i18n)
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
update({ Group }) {
|
||||||
|
this.groups = Group
|
||||||
|
this.addMarkersOnCheckPrepared()
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// description: https: //github.com/geospoc/v-mapbox/tree/v1.11.2/docs
|
||||||
|
// code example: https: //codesandbox.io/embed/v-mapbox-map-demo-k1l1n?autoresize=1&fontsize=14&hidenavigation=1&theme=dark
|
||||||
|
@import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
@import 'v-mapbox/dist/v-mapbox.css';
|
||||||
|
|
||||||
|
.mgl-map-wrapper {
|
||||||
|
height: 70vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -184,8 +184,7 @@ import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
|||||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||||
import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
|
import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
|
||||||
import { profilePagePosts } from '~/graphql/PostQuery'
|
import { profilePagePosts } from '~/graphql/PostQuery'
|
||||||
import UserQuery from '~/graphql/User'
|
import { profileUserQuery, updateUserMutation } from '~/graphql/User'
|
||||||
import { updateUserMutation } from '~/graphql/User.js'
|
|
||||||
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
||||||
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
|
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
|
||||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||||
@ -408,7 +407,7 @@ export default {
|
|||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
query() {
|
query() {
|
||||||
return UserQuery(this.$i18n)
|
return profileUserQuery(this.$i18n)
|
||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-form v-model="form" :schema="formSchema" @submit="submit">
|
<ds-form class="settings-form" v-model="form" :schema="formSchema" @submit="submit">
|
||||||
<template #default="{ errors }">
|
<template #default="{ errors }">
|
||||||
<base-card>
|
<base-card>
|
||||||
<h2 class="title">{{ $t('settings.data.name') }}</h2>
|
<h2 class="title">{{ $t('settings.data.name') }}</h2>
|
||||||
@ -22,6 +22,9 @@
|
|||||||
:loading="loadingGeo"
|
:loading="loadingGeo"
|
||||||
@input.native="handleCityInput"
|
@input.native="handleCityInput"
|
||||||
/>
|
/>
|
||||||
|
<ds-text class="location-hint" color="softer">
|
||||||
|
{{ $t('settings.data.labelCityHint') }}
|
||||||
|
</ds-text>
|
||||||
<!-- eslint-enable vue/use-v-on-exact -->
|
<!-- eslint-enable vue/use-v-on-exact -->
|
||||||
<ds-input
|
<ds-input
|
||||||
id="about"
|
id="about"
|
||||||
@ -158,3 +161,12 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// .settings-form {
|
||||||
|
// >
|
||||||
|
.location-hint {
|
||||||
|
margin-top: -$space-x-small - $space-xxx-small - $space-xxx-small;
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
</style>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ describe('my-social-media.vue', () => {
|
|||||||
let wrapper
|
let wrapper
|
||||||
let mocks
|
let mocks
|
||||||
let getters
|
let getters
|
||||||
|
let mutations
|
||||||
const socialMediaUrl = 'https://freeradical.zone/@mattwr18'
|
const socialMediaUrl = 'https://freeradical.zone/@mattwr18'
|
||||||
const newSocialMediaUrl = 'https://twitter.com/mattwr18'
|
const newSocialMediaUrl = 'https://twitter.com/mattwr18'
|
||||||
const faviconUrl = 'https://freeradical.zone/favicon.ico'
|
const faviconUrl = 'https://freeradical.zone/favicon.ico'
|
||||||
@ -30,6 +31,9 @@ describe('my-social-media.vue', () => {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
mutations = {
|
||||||
|
'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
@ -37,6 +41,7 @@ describe('my-social-media.vue', () => {
|
|||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
getters,
|
getters,
|
||||||
|
mutations,
|
||||||
})
|
})
|
||||||
return mount(MySocialMedia, { store, mocks, localVue })
|
return mount(MySocialMedia, { store, mocks, localVue })
|
||||||
}
|
}
|
||||||
@ -145,11 +150,14 @@ describe('my-social-media.vue', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deleting social media link', () => {
|
// TODO: confirm deletion modal is not present
|
||||||
|
describe.skip('deleting social media link', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
||||||
deleteButton.trigger('click')
|
deleteButton.trigger('click')
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
|
// wrapper.find('button.cancel').trigger('click')
|
||||||
|
// await Vue.nextTick()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends the link id to the backend', () => {
|
it('sends the link id to the backend', () => {
|
||||||
|
|||||||
@ -7,13 +7,8 @@
|
|||||||
:useItems="socialMediaLinks"
|
:useItems="socialMediaLinks"
|
||||||
:defaultItem="{ url: '' }"
|
:defaultItem="{ url: '' }"
|
||||||
:namePropertyKey="'url'"
|
:namePropertyKey="'url'"
|
||||||
:callbacks="{
|
:texts="mySomethingListTexts"
|
||||||
handleInput: () => {},
|
:callbacks="mySomethingListCallbacks"
|
||||||
handleInputValid,
|
|
||||||
edit: callbackEditSocialMedia,
|
|
||||||
submit: handleSubmitSocialMedia,
|
|
||||||
delete: callbackDeleteSocialMedia,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<template #list-item="{ item }">
|
<template #list-item="{ item }">
|
||||||
<social-media-list-item :item="item" />
|
<social-media-list-item :item="item" />
|
||||||
@ -33,7 +28,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import unionBy from 'lodash/unionBy'
|
import unionBy from 'lodash/unionBy'
|
||||||
import gql from 'graphql-tag'
|
import {
|
||||||
|
createSocialMediaMutation,
|
||||||
|
updateSocialMediaMutation,
|
||||||
|
deleteSocialMediaMutation,
|
||||||
|
} from '~/graphql/SocialMedia.js'
|
||||||
import MySomethingList from '~/components/_new/features/MySomethingList/MySomethingList.vue'
|
import MySomethingList from '~/components/_new/features/MySomethingList/MySomethingList.vue'
|
||||||
import SocialMediaListItem from '~/components/_new/features/SocialMedia/SocialMediaListItem.vue'
|
import SocialMediaListItem from '~/components/_new/features/SocialMedia/SocialMediaListItem.vue'
|
||||||
|
|
||||||
@ -77,6 +76,30 @@ export default {
|
|||||||
return { id, url, favicon }
|
return { id, url, favicon }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
mySomethingListTexts() {
|
||||||
|
return {
|
||||||
|
addButton: this.$t('settings.social-media.submit'),
|
||||||
|
addNew: this.$t('settings.social-media.add-new-link'),
|
||||||
|
deleteModal: {
|
||||||
|
titleIdent: 'settings.social-media.delete-modal.title',
|
||||||
|
messageIdent: 'settings.social-media.delete-modal.message',
|
||||||
|
confirm: {
|
||||||
|
icon: 'trash',
|
||||||
|
buttonTextIdent: 'settings.social-media.delete-modal.confirm-button',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: this.$t('settings.social-media.edit-link'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mySomethingListCallbacks() {
|
||||||
|
return {
|
||||||
|
handleInput: () => {},
|
||||||
|
handleInputValid: this.handleInputValid,
|
||||||
|
edit: this.callbackEditSocialMedia,
|
||||||
|
submit: this.handleSubmitSocialMedia,
|
||||||
|
delete: this.callbackDeleteSocialMedia,
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations({
|
...mapMutations({
|
||||||
@ -93,7 +116,7 @@ export default {
|
|||||||
thisList.formData.socialMediaUrl = link.url
|
thisList.formData.socialMediaUrl = link.url
|
||||||
// try to set focus on link edit field
|
// try to set focus on link edit field
|
||||||
// thisList.$refs.socialMediaUrl.$el.focus()
|
// thisList.$refs.socialMediaUrl.$el.focus()
|
||||||
// !!! Check for existenz
|
// !!! check for existence
|
||||||
// this.$scopedSlots.default()[0].context.$refs
|
// this.$scopedSlots.default()[0].context.$refs
|
||||||
// thisList.$scopedSlots['edit-item']()[0].$el.focus()
|
// thisList.$scopedSlots['edit-item']()[0].$el.focus()
|
||||||
// console.log(thisList.$scopedSlots['edit-item']()[0].context.$refs)
|
// console.log(thisList.$scopedSlots['edit-item']()[0].context.$refs)
|
||||||
@ -111,25 +134,11 @@ export default {
|
|||||||
|
|
||||||
let mutation, variables, successMessage
|
let mutation, variables, successMessage
|
||||||
if (isCreation) {
|
if (isCreation) {
|
||||||
mutation = gql`
|
mutation = createSocialMediaMutation()
|
||||||
mutation ($url: String!) {
|
|
||||||
CreateSocialMedia(url: $url) {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
variables = { url: item.url }
|
variables = { url: item.url }
|
||||||
successMessage = thisList.$t('settings.social-media.successAdd')
|
successMessage = thisList.$t('settings.social-media.successAdd')
|
||||||
} else {
|
} else {
|
||||||
mutation = gql`
|
mutation = updateSocialMediaMutation()
|
||||||
mutation ($id: ID!, $url: String!) {
|
|
||||||
UpdateSocialMedia(id: $id, url: $url) {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
variables = { id: item.id, url: item.url }
|
variables = { id: item.id, url: item.url }
|
||||||
successMessage = thisList.$t('settings.data.success')
|
successMessage = thisList.$t('settings.data.success')
|
||||||
}
|
}
|
||||||
@ -159,14 +168,7 @@ export default {
|
|||||||
async callbackDeleteSocialMedia(thisList, item) {
|
async callbackDeleteSocialMedia(thisList, item) {
|
||||||
try {
|
try {
|
||||||
await thisList.$apollo.mutate({
|
await thisList.$apollo.mutate({
|
||||||
mutation: gql`
|
mutation: deleteSocialMediaMutation(),
|
||||||
mutation ($id: ID!) {
|
|
||||||
DeleteSocialMedia(id: $id) {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: {
|
variables: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
},
|
},
|
||||||
|
|||||||
35
webapp/plugins/v-mapbox.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Vue2 + Mapbox Reference: https://github.com/geospoc/v-mapbox/issues/702
|
||||||
|
import Vue from 'vue'
|
||||||
|
import {
|
||||||
|
MglGeojsonLayer,
|
||||||
|
MglVectorLayer,
|
||||||
|
MglMap,
|
||||||
|
MglMarker,
|
||||||
|
MglPopup,
|
||||||
|
MglAttributionControl,
|
||||||
|
MglScaleControl,
|
||||||
|
MglNavigationControl,
|
||||||
|
MglGeolocateControl,
|
||||||
|
MglFullscreenControl,
|
||||||
|
} from 'v-mapbox'
|
||||||
|
|
||||||
|
// Map
|
||||||
|
Vue.component('MglMap', MglMap)
|
||||||
|
|
||||||
|
// overview of all: https://github.com/geospoc/v-mapbox/tree/v1.11.2/src/components
|
||||||
|
// mapbox: https://docs.mapbox.com/mapbox-gl-js/api/markers/
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
Vue.component('MglAttributionControl', MglAttributionControl)
|
||||||
|
Vue.component('MglScaleControl', MglScaleControl)
|
||||||
|
Vue.component('MglNavigationControl', MglNavigationControl)
|
||||||
|
Vue.component('MglGeolocateControl', MglGeolocateControl)
|
||||||
|
Vue.component('MglFullscreenControl', MglFullscreenControl)
|
||||||
|
|
||||||
|
// Layers
|
||||||
|
Vue.component('MglGeojsonLayer', MglGeojsonLayer)
|
||||||
|
Vue.component('MglVectorLayer', MglVectorLayer)
|
||||||
|
|
||||||
|
// Marker & Popup
|
||||||
|
Vue.component('MglMarker', MglMarker)
|
||||||
|
Vue.component('MglPopup', MglPopup)
|
||||||
12
webapp/static/img/mapbox/marker-icons/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Mabbox markers
|
||||||
|
|
||||||
|
I found the Mapbox markers to be downloaded at the bottom of the page:
|
||||||
|
<https://docs.mapbox.com/help/glossary/sprite/>
|
||||||
|
|
||||||
|
At URL:
|
||||||
|
<https://docs.mapbox.com/help/data/marker-icons.zip>
|
||||||
|
|
||||||
|
## Folder For Images Reachable By URL
|
||||||
|
|
||||||
|
It looks like that not all folders, as example the `assets/*` folder, is reachable by URL.
|
||||||
|
Our images have to be in the `static/img/*` folder.
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#4264fb" stroke="#314ccd" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#314ccd" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#5b7897" stroke="#23374d" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#23374d" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#33c377" stroke="#269561" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#269561" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#f79640" stroke="#ba7334" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#ba7334" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#ee4e8b" stroke="#b43b71" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#b43b71" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#7753eb" stroke="#5a3fc0" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#5a3fc0" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#f84d4d" stroke="#951212" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#951212" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||||
|
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||||
|
<g id="mapbox-marker-icon">
|
||||||
|
<g id="icon">
|
||||||
|
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||||
|
<g id="mask" opacity="0.3">
|
||||||
|
<g id="group">
|
||||||
|
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path id="color" fill="#d9d838" stroke="#a4a62d" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||||
|
<path id="circle" fill="#fff" stroke="#a4a62d" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="48" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1037
webapp/yarn.lock
@ -4422,10 +4422,10 @@ mz@^2.4.0:
|
|||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
thenify-all "^1.0.0"
|
thenify-all "^1.0.0"
|
||||||
|
|
||||||
nan@^2.9.2:
|
nan@2.17.0, nan@^2.9.2:
|
||||||
version "2.13.1"
|
version "2.17.0"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||||
integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==
|
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||||
|
|
||||||
nanomatch@^1.2.9:
|
nanomatch@^1.2.9:
|
||||||
version "1.2.13"
|
version "1.2.13"
|
||||||
|
|||||||