Merge branch 'master' into upgrade-styleguied

This commit is contained in:
Grzegorz Leoniec 2019-01-16 19:20:02 +01:00 committed by GitHub
commit 0f08ee34a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 848 additions and 311 deletions

View File

@ -12,7 +12,7 @@ scripts/
cypress/
README.md
README.md
screenshot*.png
lokalise.png
.editorconfig
.editorconfig

2
.env.template Normal file
View File

@ -0,0 +1,2 @@
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"

View File

@ -9,9 +9,11 @@ services:
- docker
env:
- DOCKER_COMPOSE_VERSION=1.23.2
- DOCKER_COMPOSE_VERSION=1.23.2 BACKEND_BRANCH=${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH:-master}}
before_install:
- echo $BACKEND_BRANCH
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
@ -21,7 +23,7 @@ install:
- docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-web .
- docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d
- git clone https://github.com/Human-Connection/Nitro-Backend.git ../Nitro-Backend
- git -C "../Nitro-Backend" checkout $TRAVIS_BRANCH || echo "Branch \`$TRAVIS_BRANCH\` does not exist, falling back to \`master\`"
- git -C "../Nitro-Backend" checkout $BACKEND_BRANCH
- docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.travis.yml up -d
- yarn global add cypress wait-on
- yarn add cypress-cucumber-preprocessor

View File

@ -1,6 +1,9 @@
FROM node:10-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000
CMD ["yarn", "run", "start"]
# Expose the app port
ARG BUILD_COMMIT
ENV BUILD_COMMIT=$BUILD_COMMIT
@ -14,6 +17,7 @@ RUN apk --no-cache add git
COPY . .
FROM base as build-and-test
RUN cp .env.template .env
RUN yarn install --production=false --frozen-lockfile --non-interactive
RUN cd styleguide && yarn install --production=false --frozen-lockfile --non-interactive \
&& cd .. \
@ -24,6 +28,3 @@ FROM base as production
ENV NODE_ENV=production
COPY --from=build-and-test ./nitro-web/node_modules ./node_modules
COPY --from=build-and-test ./nitro-web/.nuxt ./.nuxt
EXPOSE 3000
CMD ["yarn", "run", "start"]

View File

@ -15,6 +15,12 @@ $ yarn styleguide:build
$ yarn install
```
Copy:
```
cp .env.template .env
```
Configure the file `.env` according to your needs and your local setup.
### Development
``` bash
# serve with hot reload at localhost:3000

View File

@ -54,9 +54,20 @@
<hc-badges
v-if="author.badges && author.badges.length"
:badges="author.badges"
style="margin-bottom: -10px"
/>
<ds-flex>
<ds-text
v-if="author.location"
align="center"
color="soft"
size="small"
style="margin-top: 5px"
bold
>
<ds-icon name="map-marker" /> {{ author.location.name }}
</ds-text>
<ds-flex
style="margin-top: -10px"
>
<ds-flex-item class="ds-tab-nav-item">
<ds-space margin="small">
<ds-number

View File

@ -9,7 +9,7 @@
ghost
@click="$emit('click')"
>
mehr laden
{{ $t('actions.loadMore') }}
</ds-button>
</ds-space>
</template>

View File

@ -41,11 +41,13 @@
</div>
<div style="display: inline-block; float: right">
<span :style="{ opacity: post.shoutedCount ? 1 : .5 }">
<ds-icon name="bullhorn" /> <small>{{ post.shoutedCount }}</small>
<ds-icon name="bullhorn" />
<small>{{ post.shoutedCount }}</small>
</span>
&nbsp;
<span :style="{ opacity: post.commentsCount ? 1 : .5 }">
<ds-icon name="comments" /> <small>{{ post.commentsCount }}</small>
<ds-icon name="comments" />
<small>{{ post.commentsCount }}</small>
</span>
</div>
</template>

View File

@ -1,4 +1,5 @@
{
"projectId": "qa7fe2",
"ignoreTestFiles": "*.js"
"ignoreTestFiles": "*.js",
"baseUrl": "http://localhost:3000"
}

View File

@ -5,8 +5,10 @@ Feature: Authentication
Background:
Given my account has the following details:
| name | email | password |
| Peter Lustig | admin@example.org | 1234 |
| name | email | password | type
| Peter Lustig | admin@example.org | 1234 | Admin
| Bob der Bausmeister | moderator@example.org | 1234 | Moderator
| Jenny Rostock" | user@example.org | 1234 | User
Scenario: Log in
When I visit the "/login" page

View File

@ -13,12 +13,11 @@ Feature: Internationalization
Examples: Login Button
| language | buttonLabel |
| English | Login |
| Deutsch | Einloggen |
| Français | Connexion |
| Nederlands | Inloggen |
| Deutsch | Einloggen |
| English | Login |
Scenario: Keep preferred language after refresh
Given I previously switched the language to "Deutsch"
Given I previously switched the language to "Français"
And I refresh the page
Then the whole user interface appears in "Deutsch"
Then the whole user interface appears in "Français"

View File

@ -0,0 +1,45 @@
Feature: About me and and location
As a user
I would like to add some about me text and a location
So others can get some info about me and my location
The location and about me are displayed on the user profile. Later it will be possible
to search for users by location.
Background:
Given I am logged in
And I am on the "settings" page
Scenario: Change username
When I save "Hansi" as my new name
Then I can see my new name "Hansi" when I click on my profile picture in the top right
Scenario: Keep changes after refresh
When I changed my username to "Hansi" previously
And I refresh the page
Then my new username is still there
Scenario Outline: I set my location to "<location>"
When I save "<location>" as my location
And my username is "Peter Lustig"
When people visit my profile page
Then they can see the location in the info box below my avatar
Examples: Location
| location | type |
| Paris | City |
| Saxony-Anhalt | Region |
| Germany | Country |
Scenario: Display a description on profile page
Given I have the following self-description:
"""
Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei
"""
And my username is "Peter Lustig"
When people visit my profile page
Then they can see the text in the info box below my avatar

View File

@ -0,0 +1,39 @@
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
/* global cy */
const lastColumnIsSortedInDescendingOrder = () => {
cy.get('tbody')
.find('tr td:last-child')
.then(lastColumn => {
cy.wrap(lastColumn)
const values = lastColumn
.map((i, td) => parseInt(td.textContent))
.toArray()
const orderedDescending = values.slice(0).sort((a, b) => b - a)
return cy.wrap(values).should('deep.eq', orderedDescending)
})
}
When('I navigate to the administration dashboard', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
.find('a[href="/admin"]')
.click()
})
Then('I can see a list of categories ordered by post count:', table => {
// TODO: match the table in the feature with the html table
cy.get('thead')
.find('tr th')
.should('have.length', 3)
lastColumnIsSortedInDescendingOrder()
})
Then('I can see a list of tags ordered by user and post count:', table => {
// TODO: match the table in the feature with the html table
cy.get('thead')
.find('tr th')
.should('have.length', 4)
lastColumnIsSortedInDescendingOrder()
})

View File

@ -0,0 +1,76 @@
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
/* global cy */
let aboutMeText
let myLocation
let myName
const matchNameInUserMenu = name => {
cy.get('.avatar-menu').click() // open
cy.get('.avatar-menu-popover').contains(name)
cy.get('.avatar-menu').click() // close again
}
const setUserName = name => {
cy.get('input[id=name]')
.clear()
.type(name)
cy.contains('Save')
.click()
.wait(200)
myName = name
}
When('I save {string} as my new name', name => {
setUserName(name)
})
When('I save {string} as my location', location => {
cy.get('input[id=city]').type(location)
cy.get('.ds-select-option')
.contains(location)
.click()
cy.contains('Save').click()
myLocation = location
})
When('I have the following self-description:', text => {
cy.get('textarea[id=bio]')
.clear()
.type(text)
cy.contains('Save').click()
aboutMeText = text
})
When('my username is {string}', name => {
if (myName !== name) {
setUserName(name)
}
matchNameInUserMenu(name)
})
When('people visit my profile page', url => {
cy.visitMyProfile()
})
When('they can see the text in the info box below my avatar', () => {
cy.contains(aboutMeText)
})
When('I changed my username to {string} previously', name => {
myName = name
})
Then('they can see the location in the info box below my avatar', () => {
matchNameInUserMenu(myName)
})
Then('my new username is still there', () => {
matchNameInUserMenu(myName)
})
Then(
'I can see my new name {string} when I click on my profile picture in the top right',
name => matchNameInUserMenu(name)
)

View File

@ -1,63 +1,20 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { getLangByName } from '../../support/helpers'
import find from 'lodash/find'
/* global cy */
/* global cy */
const baseUrl = 'http://localhost:3000'
const username = 'Peter Lustig'
const locales = require('../../../locales')
const getLangByName = function(name) {
return find(locales, { name })
}
const openPage = function(page) {
const openPage = page => {
if (page === 'landing') {
page = ''
}
cy.visit(`${baseUrl}/${page}`)
}
const switchLanguage = function(name) {
cy.get('.login-locale-switch a').click()
cy.contains('.locale-menu-popover a', name).click()
}
const login = (email, password) => {
cy.visit(`${baseUrl}/login`)
cy.get('input[name=email]')
.trigger('focus')
.type(email)
cy.get('input[name=password]')
.trigger('focus')
.type(password)
cy.get('button[name=submit]')
.as('submitButton')
.click()
cy.location('pathname').should('eq', '/') // we're in!
}
const logout = () => {
cy.visit(`${baseUrl}/logout`)
cy.location('pathname').should('contain', '/login') // we're out
}
const lastColumnIsSortedInDescendingOrder = () => {
cy.get('tbody')
.find('tr td:last-child')
.then(last_column => {
cy.wrap(last_column)
const values = last_column
.map((i, td) => parseInt(td.textContent))
.toArray()
const ordered_descending = values.slice(0).sort((a, b) => b - a)
return cy.wrap(values).should('deep.eq', ordered_descending)
})
cy.visit(`/${page}`)
}
Given('I am logged in', () => {
login('admin@example.org', 1234)
cy.login('admin@example.org', 1234)
})
Given('we have a selection of tags and categories as well as posts', () => {
@ -72,7 +29,7 @@ Given('my user account has the role {string}', role => {
// TODO: use db factories instead of seed data
})
When('I log out', logout)
When('I log out', cy.logout)
When('I visit the {string} page', page => {
openPage(page)
@ -82,7 +39,7 @@ Given('I am on the {string} page', page => {
})
When('I fill in my email and password combination and click submit', () => {
login('admin@example.org', 1234)
cy.login('admin@example.org', 1234)
})
When('I refresh the page', () => {
@ -92,8 +49,7 @@ When('I refresh the page', () => {
When('I log out through the menu in the top right corner', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
.find('a')
.contains('Logout')
.find('a[href="/logout"]')
.click()
})
@ -108,7 +64,6 @@ Then('I can see my name {string} in the dropdown menu', () => {
Then('I see the login screen again', () => {
cy.location('pathname').should('contain', '/login')
cy.contains('If you already have a human-connection account, login here.')
})
Then('I am still logged in', () => {
@ -117,10 +72,10 @@ Then('I am still logged in', () => {
})
When('I select {string} in the language menu', name => {
switchLanguage(name)
cy.switchLanguage(name, true)
})
Given('I previously switched the language to {string}', name => {
switchLanguage(name)
cy.switchLanguage(name, true)
})
Then('the whole user interface appears in {string}', name => {
const lang = getLangByName(name)
@ -131,29 +86,10 @@ Then('I see a button with the label {string}', label => {
cy.contains('button', label)
})
When('I navigate to the administration dashboard', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
.contains('Admin')
.click()
})
When(`I click on {string}`, linkOrButton => {
cy.contains(linkOrButton).click()
})
Then('I can see a list of categories ordered by post count:', table => {
// TODO: match the table in the feature with the html table
cy.get('thead')
.find('tr th')
.should('have.length', 3)
lastColumnIsSortedInDescendingOrder()
})
Then('I can see a list of tags ordered by user and post count:', table => {
// TODO: match the table in the feature with the html table
cy.get('thead')
.find('tr th')
.should('have.length', 4)
lastColumnIsSortedInDescendingOrder()
When('I press {string}', label => {
cy.contains(label).click()
})

View File

@ -10,16 +10,64 @@
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
// Cypress.Commands.add('login', (email, password) => { ... })
/* globals Cypress cy */
import { getLangByName } from './helpers'
const switchLang = name => {
cy.get('.locale-menu').click()
cy.contains('.locale-menu-popover a', name).click()
}
Cypress.Commands.add('switchLanguage', (name, force) => {
const code = getLangByName(name).code
if (force) {
switchLang(name)
} else {
cy.get('html').then($html => {
if ($html && $html.attr('lang') !== code) {
switchLang(name)
}
})
}
})
Cypress.Commands.add('visitMyProfile', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
.find('a[href^="/profile/"]')
.click()
})
Cypress.Commands.add('login', (email, password) => {
cy.visit(`/login`)
cy.get('input[name=email]')
.trigger('focus')
.type(email)
cy.get('input[name=password]')
.trigger('focus')
.type(password)
cy.get('button[name=submit]')
.as('submitButton')
.click()
cy.location('pathname').should('eq', '/') // we're in!
})
Cypress.Commands.add('logout', (email, password) => {
cy.visit(`/logout`)
cy.location('pathname').should('contain', '/login') // we're out
})
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

16
cypress/support/config.js Normal file
View File

@ -0,0 +1,16 @@
export default {
users: {
admin: {
email: 'admin@example.org',
password: 1234
},
moderator: {
email: 'moderator@example.org',
password: 1234
},
user: {
email: 'user@example.org',
password: 1234
}
}
}

View File

@ -0,0 +1,11 @@
import find from 'lodash/find'
const helpers = {
locales: require('../../locales'),
getLangByName: name => {
return find(helpers.locales, { name })
}
}
export default helpers

View File

@ -14,6 +14,8 @@ services:
environment:
- HOST=0.0.0.0
- BACKEND_URL=http://backend:4000
- JWT_SECRET="b/&&7b78BF&fv/Vd"
- MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ"
networks:
hc-network:

View File

@ -1,72 +1,89 @@
import gql from 'graphql-tag'
export default gql(`
query User($slug: String!, $first: Int, $offset: Int) {
User(slug: $slug) {
id
name
avatar
createdAt
badges {
id
key
icon
}
badgesCount
shoutedCount
commentsCount
followingCount
following(first: 7) {
export default app => {
const lang = app.$i18n.locale().toUpperCase()
return gql(`
query User($slug: String!, $first: Int, $offset: Int) {
User(slug: $slug) {
id
name
slug
avatar
followedByCount
contributionsCount
commentsCount
about
locationName
location {
name: name${lang}
}
createdAt
badges {
id
key
icon
}
}
followedByCount
followedBy(first: 7) {
id
name
slug
avatar
followedByCount
contributionsCount
commentsCount
badges {
id
key
icon
}
}
contributionsCount
contributions(first: $first, offset: $offset, orderBy: createdAt_desc) {
id
slug
title
contentExcerpt
badgesCount
shoutedCount
commentsCount
deleted
image
createdAt
categories {
followingCount
following(first: 7) {
id
name
icon
}
author {
id
slug
avatar
followedByCount
contributionsCount
commentsCount
badges {
id
key
icon
}
location {
name: name${lang}
}
}
followedByCount
followedBy(first: 7) {
id
name
slug
avatar
followedByCount
contributionsCount
commentsCount
badges {
id
key
icon
}
location {
name: name${lang}
}
}
contributionsCount
contributions(first: $first, offset: $offset, orderBy: createdAt_desc) {
id
slug
title
contentExcerpt
shoutedCount
commentsCount
deleted
image
createdAt
categories {
id
name
icon
}
author {
id
avatar
name
location {
name: name${lang}
}
}
}
}
}
}
`)
`)
}

View File

@ -47,6 +47,15 @@
>
<div class="avatar-menu-popover">
{{ $t('login.hello') }} <b>{{ user.name }}</b>
<template v-if="user.role !== 'user'">
<ds-text
color="softer"
size="small"
style="margin-bottom: 0"
>
{{ user.role | camelCase }}
</ds-text>
</template>
<hr>
<ds-menu
:routes="routes"

View File

@ -1,4 +1,12 @@
{
"actions": {
"loading": "lade",
"loadMore": "mehr laden",
"create": "Erstellen",
"save": "Speichern",
"edit": "Bearbeiten",
"delete": "Löschen"
},
"login": {
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
"login": "Einloggen",
@ -20,7 +28,10 @@
"settings": {
"name": "Einstellungen",
"data": {
"name": "Deine Daten"
"name": "Deine Daten",
"labelName": "Dein Name",
"labelCity": "Deine Stadt oder Region",
"labelBio": "Über dich"
},
"security": {
"name": "Sicherheit"

View File

@ -1,4 +1,12 @@
{
"actions": {
"loading": "loading",
"loadMore": "load more",
"create": "Create",
"save": "Save",
"edit": "Edit",
"delete": "Delete"
},
"login": {
"copy": "If you already have a human-connection account, login here.",
"login": "Login",
@ -20,7 +28,10 @@
"settings": {
"name": "Settings",
"data": {
"name": "Your data"
"name": "Your data",
"labelName": "Your Name",
"labelCity": "Your City or Region",
"labelBio": "About You"
},
"security": {
"name": "Security"

View File

@ -1,5 +1,5 @@
const pkg = require('./package')
const envWhitelist = ['NODE_ENV', 'MAINTENANCE']
const envWhitelist = ['NODE_ENV', 'MAINTENANCE', 'MAPBOX_TOKEN']
const dev = process.env.NODE_ENV !== 'production'
const path = require('path')
@ -62,6 +62,7 @@ module.exports = {
*/
plugins: [
{ src: '~/plugins/i18n.js', ssr: true },
{ src: '~/plugins/axios.js', ssr: false },
{ src: '~/plugins/keep-alive.js', ssr: false },
{ src: '~/plugins/design-system.js', ssr: true },
{ src: '~/plugins/vue-directives.js', ssr: false },

View File

@ -79,40 +79,45 @@ export default {
},
apollo: {
Post: {
query: gql(`
query Post($first: Int, $offset: Int) {
Post(first: $first, offset: $offset) {
id
title
contentExcerpt
createdAt
slug
image
author {
query() {
return gql(`
query Post($first: Int, $offset: Int) {
Post(first: $first, offset: $offset) {
id
avatar
title
contentExcerpt
createdAt
slug
name
contributionsCount
shoutedCount
commentsCount
followedByCount
badges {
image
author {
id
key
avatar
slug
name
contributionsCount
shoutedCount
commentsCount
followedByCount
location {
name: name${this.$i18n.locale().toUpperCase()}
}
badges {
id
key
icon
}
}
commentsCount
categories {
id
name
icon
}
shoutedCount
}
commentsCount
categories {
id
name
icon
}
shoutedCount
}
}
`),
`)
},
variables() {
return {
first: this.pageSize,

View File

@ -141,39 +141,16 @@ export default {
},
apollo: {
Post: {
query: gql(`
query Post($slug: String!) {
Post(slug: $slug) {
id
title
content
createdAt
slug
image
author {
query() {
return gql(`
query Post($slug: String!) {
Post(slug: $slug) {
id
slug
name
avatar
shoutedCount
contributionsCount
commentsCount
followedByCount
badges {
id
key
icon
}
}
tags {
name
}
commentsCount
comments(orderBy: createdAt_desc) {
id
contentExcerpt
title
content
createdAt
deleted
slug
image
author {
id
slug
@ -183,22 +160,53 @@ export default {
contributionsCount
commentsCount
followedByCount
location {
name: name${this.$i18n.locale().toUpperCase()}
}
badges {
id
key
icon
}
}
tags {
name
}
commentsCount
comments(orderBy: createdAt_desc) {
id
contentExcerpt
createdAt
deleted
author {
id
slug
name
avatar
shoutedCount
contributionsCount
commentsCount
followedByCount
location {
name: name${this.$i18n.locale().toUpperCase()}
}
badges {
id
key
icon
}
}
}
categories {
id
name
icon
}
shoutedCount
}
categories {
id
name
icon
}
shoutedCount
}
}
`),
`)
},
variables() {
return {
slug: this.$route.params.slug

View File

@ -7,13 +7,16 @@
<ds-space />
<h3><ds-icon name="compass" /> Themenkategorien</h3>
<div class="tags">
<ds-tag
<ds-icon
v-for="category in post.categories"
:key="category.id"
v-tooltip="{content: category.name, placement: 'top-start', delay: { show: 300 }}"
>
<ds-icon :name="category.icon" /> {{ category.name }}
</ds-tag>
:name="category.icon"
size="large"
/>&nbsp;
<!--<ds-tag
v-for="category in post.categories"
:key="category.id"><ds-icon :name="category.icon" /> {{ category.name }}</ds-tag>-->
</div>
<template v-if="post.tags && post.tags.length">
<h3><ds-icon name="tags" /> Schlagwörter</h3>
@ -70,51 +73,56 @@ export default {
},
apollo: {
Post: {
query: gql(`
query Post($slug: String!) {
Post(slug: $slug) {
id
title
tags {
id
name
}
categories {
id
name
icon
}
relatedContributions(first: 2) {
query() {
return gql(`
query Post($slug: String!) {
Post(slug: $slug) {
id
title
slug
contentExcerpt
shoutedCount
commentsCount
tags {
id
name
}
categories {
id
name
icon
}
author {
relatedContributions(first: 2) {
id
name
title
slug
avatar
contributionsCount
followedByCount
contentExcerpt
shoutedCount
commentsCount
badges {
categories {
id
key
name
icon
}
author {
id
name
slug
avatar
contributionsCount
followedByCount
commentsCount
location {
name: name${this.$i18n.locale().toUpperCase()}
}
badges {
id
key
icon
}
}
}
shoutedCount
}
shoutedCount
}
}
`),
`)
},
variables() {
return {
slug: this.$route.params.slug

View File

@ -25,6 +25,14 @@
>
{{ user.name }}
</ds-heading>
<ds-text
v-if="user.location"
align="center"
color="soft"
size="small"
>
<ds-icon name="map-marker" /> {{ user.location.name }}
</ds-text>
<ds-text
align="center"
color="soft"
@ -72,6 +80,20 @@
@update="voted = true && fetchUser()"
/>
</ds-space>
<template v-if="user.about">
<hr>
<ds-space
margin-top="small"
margin-bottom="small"
>
<ds-text
color="soft"
size="small"
>
{{ user.about }}
</ds-text>
</ds-space>
</template>
</ds-card>
<ds-space />
<ds-heading
@ -176,7 +198,7 @@
:width="{ base: '100%' }"
gutter="small"
>
<ds-flex-item>
<ds-flex-item class="profile-top-navigation">
<ds-card class="ds-tab-nav">
<ds-flex>
<ds-flex-item class="ds-tab-nav-item ds-tab-nav-item-active">
@ -338,7 +360,9 @@ export default {
},
apollo: {
User: {
query: require('~/graphql/UserProfileQuery.js').default,
query() {
return require('~/graphql/UserProfileQuery.js').default(this)
},
variables() {
return {
slug: this.$route.params.slug,
@ -360,6 +384,12 @@ export default {
border: #fff 5px solid;
}
.profile-top-navigation {
position: sticky;
top: 53px;
z-index: 1;
}
.ds-tab-nav {
.ds-card-content {
padding: 0 !important;

View File

@ -1,7 +1,204 @@
<template>
<ds-card>
<ds-space margin="small">
<ds-card space="small">
<ds-heading tag="h3">
{{ $t('settings.data.name') }}
</ds-space>
</ds-heading>
<ds-input
id="name"
v-model="form.name"
icon="user"
:label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.labelName')"
/>
<!-- eslint-disable vue/use-v-on-exact -->
<ds-select
id="city"
v-model="form.locationName"
:options="cities"
icon="map-marker"
:label="$t('settings.data.labelCity')"
:placeholder="$t('settings.data.labelCity')"
@input.native="handleCityInput"
/>
<!-- eslint-enable vue/use-v-on-exact -->
<ds-input
id="bio"
v-model="form.about"
type="textarea"
rows="3"
:label="$t('settings.data.labelBio')"
:placeholder="$t('settings.data.labelBio')"
/>
<template slot="footer">
<ds-button
style="float: right;"
icon="check"
primary
@click.prevent="submit"
>
{{ $t('actions.save') }}
</ds-button>
</template>
</ds-card>
</template>
<script>
import gql from 'graphql-tag'
import { mapGetters } from 'vuex'
import { CancelToken } from 'axios'
import find from 'lodash/find'
let timeout
const mapboxToken = process.env.MAPBOX_TOKEN
export default {
data() {
return {
axiosSource: null,
cities: [],
form: {
name: null,
locationName: null,
about: null
}
}
},
computed: {
...mapGetters({
user: 'auth/user'
})
},
watch: {
user: {
immediate: true,
handler: function(user) {
this.form = {
name: user.name,
locationName: user.locationName,
about: user.about
}
}
}
},
methods: {
submit() {
console.log('SUBMIT', { ...this.form })
this.$apollo
.mutate({
mutation: gql`
mutation(
$id: ID!
$name: String
$locationName: String
$about: String
) {
UpdateUser(
id: $id
name: $name
locationName: $locationName
about: $about
) {
id
name
locationName
about
}
}
`,
// Parameters
variables: {
id: this.user.id,
name: this.form.name,
locationName: this.form.locationName,
about: this.form.about
},
// Update the cache with the result
// The query will be updated with the optimistic response
// and then with the real result of the mutation
update: (store, { data: { UpdateUser } }) => {
this.$store.dispatch('auth/fetchCurrentUser')
// Read the data from our cache for this query.
// const data = store.readQuery({ query: TAGS_QUERY })
// Add our tag from the mutation to the end
// data.tags.push(addTag)
// Write our data back to the cache.
// store.writeQuery({ query: TAGS_QUERY, data })
}
// Optimistic UI
// Will be treated as a 'fake' result as soon as the request is made
// so that the UI can react quickly and the user be happy
/* optimisticResponse: {
__typename: 'Mutation',
addTag: {
__typename: 'Tag',
id: -1,
label: newTag
}
} */
})
.then(data => {
console.log(data)
this.$toast.success('Updated user')
})
.catch(err => {
console.error(err)
this.$toast.error(err.message)
})
},
handleCityInput(value) {
clearTimeout(timeout)
timeout = setTimeout(() => this.requestGeoData(value), 500)
},
processCityResults(res) {
if (
!res ||
!res.data ||
!res.data.features ||
!res.data.features.length
) {
return []
}
let output = []
res.data.features.forEach(item => {
output.push({
label: item.place_name,
value: item.place_name,
id: item.id
})
})
return output
},
requestGeoData(e) {
if (this.axiosSource) {
// cancel last request
this.axiosSource.cancel()
}
const value = e.target ? e.target.value.trim() : ''
if (value === '' || value.length < 3) {
this.cities = []
return
}
this.axiosSource = CancelToken.source()
const place = encodeURIComponent(value)
const lang = this.$i18n.locale()
this.$axios
.get(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${mapboxToken}&types=region,place,country&language=${lang}`,
{
cancelToken: this.axiosSource.token
}
)
.then(res => {
this.cities = this.processCityResults(res)
})
}
}
}
</script>

View File

@ -1,4 +1,4 @@
export default function({ app }) {
export default ({ app }) => {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:4000'
return {
httpEndpoint: process.server ? backendUrl : '/api',

7
plugins/axios.js Normal file
View File

@ -0,0 +1,7 @@
export default ({ $axios, app }) => {
$axios.onRequest(config => {
console.log(Object.keys(app))
// add current ui language
config.headers['Accept-Language'] = app.$i18n.locale()
})
}

View File

@ -67,9 +67,12 @@ export default ({ app, req, cookie, store }) => {
if (!isEmpty(localeCookie)) {
userLocale = localeCookie
} else {
userLocale = process.browser
? navigator.language || navigator.userLanguage
: req.locale
try {
userLocale = process.browser
? navigator.language || navigator.userLanguage
: req.headers['accept-language'].split(',')[0]
} catch (err) {}
if (userLocale && !isEmpty(userLocale.language)) {
userLocale = userLocale.language.substr(0, 2)
}

View File

@ -2,27 +2,30 @@ import Vue from 'vue'
let lastRoute
const keepAliveHook = {}
keepAliveHook.install = Vue => {
const keepAlivePages = process.env.keepAlivePages || []
Vue.mixin({
// Save route if this instance is a page (has metaInfo)
mounted() {
if (this.$metaInfo) {
lastRoute = this.$route.name
if (!process.server) {
keepAliveHook.install = Vue => {
const keepAlivePages = process.env.keepAlivePages || []
Vue.mixin({
// Save route if this instance is a page (has metaInfo)
mounted() {
if (this.$metaInfo) {
lastRoute = this.$route.name
}
},
activated() {
if (this.$metaInfo) {
lastRoute = this.$route.name
}
},
deactivated() {
// If this is a page and we don't want it to be kept alive
if (this.$metaInfo && !keepAlivePages.includes(lastRoute)) {
this.$destroy()
}
}
},
activated() {
if (this.$metaInfo) {
lastRoute = this.$route.name
}
},
deactivated() {
// If this is a page and we don't want it to be kept alive
if (this.$metaInfo && !keepAlivePages.includes(lastRoute)) {
this.$destroy()
}
}
})
})
}
Vue.use(keepAliveHook)
}
Vue.use(keepAliveHook)

View File

@ -2,9 +2,11 @@ const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()
require('dotenv').config()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000
app.set('port', port)
// Import and Set Nuxt.js options

View File

@ -49,8 +49,8 @@ export const getters = {
}
export const actions = {
async init({ commit }) {
if (process.client) {
async init({ commit, dispatch }) {
if (!process.server) {
return
}
const token = this.app.$apolloHelpers.getToken()
@ -58,18 +58,15 @@ export const actions = {
return
}
const user = await jwt.verify(token, 'b/&&7b78BF&fv/Vd')
if (user.id) {
commit('SET_USER', {
id: user.id,
name: user.name,
slug: user.slug,
email: user.email,
avatar: user.avatar,
role: user.role
})
commit('SET_TOKEN', token)
const payload = await jwt.verify(token, process.env.JWT_SECRET)
if (!payload.id) {
return
}
commit('SET_TOKEN', token)
commit('SET_USER', {
id: payload.id
})
await dispatch('fetchCurrentUser')
},
async check({ commit, dispatch, getters }) {
if (!this.app.$apolloHelpers.getToken()) {
@ -77,24 +74,53 @@ export const actions = {
}
return getters.isLoggedIn
},
async login({ commit }, { email, password }) {
commit('SET_PENDING', true)
try {
const res = await this.app.apolloProvider.defaultClient
.mutate({
mutation: gql(`
mutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
async fetchCurrentUser({ commit, getters }) {
await this.app.apolloProvider.defaultClient
.query({
query: gql(`
query User($id: ID!) {
User(id: $id) {
id
name
slug
email
avatar
role
token
locationName
about
}
}
`),
variables: { id: getters.user.id }
})
.then(({ data }) => {
const user = data.User.pop()
if (user.id && user.email) {
commit('SET_USER', user)
}
})
return getters.user
},
async login({ commit }, { email, password }) {
commit('SET_PENDING', true)
try {
const res = await this.app.apolloProvider.defaultClient
.mutate({
mutation: gql(`
mutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
id
name
slug
email
avatar
role
locationName
about
token
}
}
`),
variables: { email, password }
})
.then(({ data }) => data && data.login)

View File

@ -58,7 +58,7 @@
"vue-router": "^3.0.1",
"vue-svg-loader": "^0.8.0",
"vue-template-compiler": "^2.5.17",
"vuep": "git://github.com/visualjerk/vuep.git#fix-iframe-firefox",
"vuep": "git+https://github.com/visualjerk/vuep.git#fix-iframe-firefox",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-merge-and-include-globally": "^2.0.11"
},

View File

@ -10629,9 +10629,9 @@ vue@^2.4.2, vue@^2.5.17:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.17.tgz#0f8789ad718be68ca1872629832ed533589c6ada"
integrity sha512-mFbcWoDIJi0w0Za4emyLiW72Jae0yjANHbCVquMKijcavBGypqlF7zHRgMa5k4sesdv7hv2rB4JPdZfR+TPfhQ==
"vuep@git://github.com/visualjerk/vuep.git#fix-iframe-firefox":
"vuep@git+https://github.com/visualjerk/vuep.git#fix-iframe-firefox":
version "0.8.1"
resolved "git://github.com/visualjerk/vuep.git#df765f9bce3d96f79ffc35e75ec2885539bf9baa"
resolved "git+https://github.com/visualjerk/vuep.git#df765f9bce3d96f79ffc35e75ec2885539bf9baa"
dependencies:
simple-assign "^0.1.0"