diff --git a/.dockerignore b/.dockerignore index 4ab35e506..cd8fd3390 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,7 +12,7 @@ scripts/ cypress/ -README.md +README.md screenshot*.png lokalise.png -.editorconfig \ No newline at end of file +.editorconfig diff --git a/.env.template b/.env.template new file mode 100644 index 000000000..4313645bb --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +JWT_SECRET="b/&&7b78BF&fv/Vd" +MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" diff --git a/.travis.yml b/.travis.yml index 46a173ae7..241fb2577 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 8f7df318b..abbf2917b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index b82c75999..3a563eff9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/components/Author.vue b/components/Author.vue index c9b5ecec8..f1f57113b 100644 --- a/components/Author.vue +++ b/components/Author.vue @@ -54,9 +54,20 @@ - + + {{ author.location.name }} + + - mehr laden + {{ $t('actions.loadMore') }} diff --git a/components/PostCard.vue b/components/PostCard.vue index 450e69959..6d6f56256 100644 --- a/components/PostCard.vue +++ b/components/PostCard.vue @@ -41,11 +41,13 @@
- {{ post.shoutedCount }} + + {{ post.shoutedCount }}   - {{ post.commentsCount }} + + {{ post.commentsCount }}
diff --git a/cypress.json b/cypress.json index af4800ea3..f41489007 100644 --- a/cypress.json +++ b/cypress.json @@ -1,4 +1,5 @@ { "projectId": "qa7fe2", - "ignoreTestFiles": "*.js" + "ignoreTestFiles": "*.js", + "baseUrl": "http://localhost:3000" } diff --git a/cypress/integration/Login.feature b/cypress/integration/01.Login.feature similarity index 71% rename from cypress/integration/Login.feature rename to cypress/integration/01.Login.feature index a7a47ddd5..72adc8553 100644 --- a/cypress/integration/Login.feature +++ b/cypress/integration/01.Login.feature @@ -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 diff --git a/cypress/integration/Internationalization.feature b/cypress/integration/02.Internationalization.feature similarity index 83% rename from cypress/integration/Internationalization.feature rename to cypress/integration/02.Internationalization.feature index 375fc03aa..0a5f90ff0 100644 --- a/cypress/integration/Internationalization.feature +++ b/cypress/integration/02.Internationalization.feature @@ -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" diff --git a/cypress/integration/TagsAndCategories.feature b/cypress/integration/03.TagsAndCategories.feature similarity index 100% rename from cypress/integration/TagsAndCategories.feature rename to cypress/integration/03.TagsAndCategories.feature diff --git a/cypress/integration/04.AboutMeAndLocation.feature b/cypress/integration/04.AboutMeAndLocation.feature new file mode 100644 index 000000000..83e7046f5 --- /dev/null +++ b/cypress/integration/04.AboutMeAndLocation.feature @@ -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 "" + When I save "" 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 + + + + diff --git a/cypress/integration/common/admin.js b/cypress/integration/common/admin.js new file mode 100644 index 000000000..9162667b4 --- /dev/null +++ b/cypress/integration/common/admin.js @@ -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() +}) diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js new file mode 100644 index 000000000..048f37b7a --- /dev/null +++ b/cypress/integration/common/settings.js @@ -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) +) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 78266d96e..ee9a364f7 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -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() }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index c1f5a772e..77c75c7d5 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -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) => { ... }) diff --git a/cypress/support/config.js b/cypress/support/config.js new file mode 100644 index 000000000..af96ad615 --- /dev/null +++ b/cypress/support/config.js @@ -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 + } + } +} diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js new file mode 100644 index 000000000..661682139 --- /dev/null +++ b/cypress/support/helpers.js @@ -0,0 +1,11 @@ + +import find from 'lodash/find' + +const helpers = { + locales: require('../../locales'), + getLangByName: name => { + return find(helpers.locales, { name }) + } +} + +export default helpers diff --git a/docker-compose.yml b/docker-compose.yml index c8bba8595..5a4c2ab5d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/graphql/UserProfileQuery.js b/graphql/UserProfileQuery.js index 6374fd228..4e0245b52 100644 --- a/graphql/UserProfileQuery.js +++ b/graphql/UserProfileQuery.js @@ -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} + } + } } } } - } -`) + `) +} diff --git a/layouts/default.vue b/layouts/default.vue index 5080ac93b..a44da3422 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -47,6 +47,15 @@ >
{{ $t('login.hello') }} {{ user.name }} +

Themenkategorien

- - {{ category.name }} - + :name="category.icon" + size="large" + />  +