merged master in

This commit is contained in:
Grzegorz Leoniec 2018-12-29 16:09:06 +01:00
commit bbe1d4e7c3
No known key found for this signature in database
GPG Key ID: 3AA43686D4EB1377
77 changed files with 3235 additions and 404 deletions

View File

@ -6,5 +6,8 @@ npm-debug.log
Dockerfile
docker-compose*.yml
scripts/
.env
cypress/

4
.gitignore vendored
View File

@ -72,6 +72,10 @@ dist
# IDE
.idea
.vscode
# TEMORIRY
static/uploads
cypress/videos
cypress/screenshots/

View File

@ -1,28 +1,42 @@
language: node_js
node_js:
- "10"
services:
- docker
cache:
yarn: true
directories:
- node_modules
services:
- docker
env:
- DOCKER_COMPOSE_VERSION=1.23.2
before_install:
- 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
- sudo mv docker-compose /usr/local/bin
install:
- docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT -t humanconnection/nitro-web .
- docker-compose -f docker-compose.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\`"
- 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
script:
- docker run humanconnection/nitro-web yarn run lint
- docker-compose exec webapp yarn run lint
- docker-compose exec webapp yarn run test
- docker-compose -f ../Nitro-Backend/docker-compose.yml exec backend yarn run db:seed > /dev/null
- wait-on http://localhost:3000
- cypress run --record --key $CYPRESS_TOKEN
after_success:
# - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
# - chmod +x send.sh
# - ./send.sh success $WEBHOOK_URL
- if [ $TRAVIS_BRANCH == "master" ] && [ $TRAVIS_EVENT_TYPE == "push" ]; then
docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD";
docker tag humanconnection/nitro-web humanconnection/nitro-web:latest;
docker push humanconnection/nitro-web:latest;
fi
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
- chmod +x send.sh
- ./send.sh success $WEBHOOK_URL
after_failure:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
@ -30,6 +44,10 @@ after_failure:
- ./send.sh failure $WEBHOOK_URL
deploy:
- provider: script
script: scripts/docker_push.sh
on:
branch: master
- provider: script
script: scripts/deploy.sh nitro.human-connection.org
on:

View File

@ -1,10 +1,12 @@
# Human Connection - NITRO Web
[![Build Status](https://travis-ci.com/Human-Connection/Nitro-Web.svg?branch=master)](https://travis-ci.com/Human-Connection/Nitro-Web)
![UI Screenshot](screenshot.png)
## Build Setup
### Install
``` bash
# install all dependencies
@ -36,4 +38,16 @@ All reusable Components (for example avatar) should be done inside the styleguid
### To show the styleguide
``` bash
$ yarn styleguide
```
```
## Internationalization (i18n)
You can help translating the interface by joining us on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/).
Thanks lokalise.co that we can use your premium account!
<a href="(https://lokalise.co/public/556252725c18dd752dd546.13222042/)."><img src="lokalise.png" alt="localise.co" height="32px" /></a>
## Attributions
<div>Locale Icons made by <a href="http://www.freepik.com/" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div>

67
components/Dropdown.vue Normal file
View File

@ -0,0 +1,67 @@
<template>
<v-popover
:open.sync="isPopoverOpen"
:open-group="Math.random().toString()"
:placement="placement"
trigger="manual"
:offset="offset"
>
<slot :toggleMenu="toggleMenu" />
<div
slot="popover"
@mouseover="popoverMouseEnter"
@mouseleave="popoveMouseLeave"
>
<slot
name="popover"
:toggleMenu="toggleMenu"
/>
</div>
</v-popover>
</template>
<script>
import { mapGetters } from 'vuex'
let mouseEnterTimer = null
let mouseLeaveTimer = null
export default {
props: {
placement: { type: String, default: 'bottom-end' },
offset: { type: [String, Number], default: '16' }
},
data() {
return {
isPopoverOpen: false
}
},
beforeDestroy() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
},
methods: {
toggleMenu() {
this.isPopoverOpen = !this.isPopoverOpen
},
popoverMouseEnter() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (!this.isPopoverOpen) {
mouseEnterTimer = setTimeout(() => {
this.isPopoverOpen = true
}, 500)
}
},
popoveMouseLeave() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (this.isPopoverOpen) {
mouseLeaveTimer = setTimeout(() => {
this.isPopoverOpen = false
}, 300)
}
}
}
}
</script>

114
components/LocaleSwitch.vue Normal file
View File

@ -0,0 +1,114 @@
<template>
<dropdown
ref="menu"
:placement="placement"
:offset="offset"
>
<template
slot="default"
slot-scope="{toggleMenu}"
>
<a
class="locale-menu"
href="#"
@click.prevent="toggleMenu()"
>
<img
:alt="current.name"
:title="current.name"
:src="`/img/locale-flags/${current.code}.svg`"
height="26"
>
</a>
</template>
<template slot="popover">
<ul class="locale-menu-popover">
<li
v-for="locale in locales"
:key="locale.code"
>
<a
href="#"
style="display: flex; align-items: center;"
:class="[
locale.code,
current.code === locale.code && 'active'
]"
@click.prevent="changeLanguage(locale.code)"
>
<img
:alt="locale.name"
:title="locale.name"
:src="`/img/locale-flags/${locale.code}.svg`"
width="20"
> {{ locale.name }}
</a>
</li>
</ul>
</template>
</dropdown>
</template>
<script>
import Dropdown from '~/components/Dropdown'
import find from 'lodash/find'
export default {
components: {
Dropdown
},
props: {
placement: { type: String, default: 'bottom-start' },
offset: { type: [String, Number], default: '16' }
},
data() {
return {
locales: process.env.locales
}
},
computed: {
current() {
return find(this.locales, { code: this.$i18n.locale() })
}
},
methods: {
changeLanguage(locale) {
this.$i18n.set(locale)
this.$refs.menu.toggleMenu()
}
}
}
</script>
<style lang="scss">
.locale-menu {
user-select: none;
}
ul.locale-menu-popover {
list-style: none;
padding: 0;
margin: 0;
li {
a {
opacity: 0.8;
display: block;
padding: 0.3rem 0;
img {
margin-right: 8px;
}
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
font-weight: bold;
}
}
}
}
</style>

9
components/mixins/seo.js Normal file
View File

@ -0,0 +1,9 @@
export default {
head() {
return {
htmlAttrs: {
lang: this.$i18n.locale()
}
}
}
}

4
cypress.json Normal file
View File

@ -0,0 +1,4 @@
{
"projectId": "qa7fe2",
"ignoreTestFiles": "*.js"
}

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,24 @@
Feature: Internationalization
As a user who is not very fluent in English
I would like to see the user interface translated to my preferred language
In order to be able to understand the interface
Background:
Given I am on the "login" page
Scenario Outline: I select "<language>" in the language menu and see "<buttonLabel>"
When I select "<language>" in the language menu
Then the whole user interface appears in "<language>"
Then I see a button with the label "<buttonLabel>"
Examples: Login Button
| language | buttonLabel |
| English | Login |
| Deutsch | Einloggen |
| Français | Connexion |
| Nederlands | Inloggen |
Scenario: Keep preferred language after refresh
Given I previously switched the language to "Deutsch"
And I refresh the page
Then the whole user interface appears in "Deutsch"

View File

@ -0,0 +1,25 @@
Feature: Authentication
As a database administrator
I want users to sign in
In order to attribute posts and other contributions to their authors
Background:
Given my account has the following details:
| name | email | password |
| Peter Lustig | admin@example.org | 1234 |
Scenario: Log in
When I visit the "/login" page
And I fill in my email and password combination and click submit
Then I can click on my profile picture in the top right corner
And I can see my name "Peter Lustig" in the dropdown menu
Scenario: Refresh and stay logged in
Given I am logged in
When I refresh the page
Then I am still logged in
Scenario: Log out
Given I am logged in
When I log out through the menu in the top right corner
Then I see the login screen again

View File

@ -0,0 +1,41 @@
Feature: Tags and Categories
As a database administrator
I would like to see a summary of all tags and categories and their usage
In order to be able to decide which tags and categories are popular or not
The currently deployed application, codename "Alpha", distinguishes between
categories and tags. Each post can have a number of categories and/or tags.
A few categories are required for each post, tags are completely optional.
Both help to find relevant posts in the database, e.g. users can filter for
categories.
If administrators summary of all tags and categories and how often they are
used, they learn what new category might be convenient for users, e.g. by
looking at the popularity of a tag.
Background:
Given we have a selection of tags and categories as well as posts
And my user account has the role "administrator"
Given I am logged in
Scenario: See an overview of categories
When I navigate to the administration dashboard
And I click on "Categories"
Then I can see a list of categories ordered by post count:
| Icon | Name | Post Count |
| | Just For Fun | 5 |
| | Happyness & Values | 2 |
| | Health & Wellbeing | 1 |
Scenario: See an overview of tags
When I navigate to the administration dashboard
And I click on "Tags"
Then I can see a list of tags ordered by user and post count:
| # | Name | Nutzer | Beiträge |
| 1 | Naturschutz | 2 | 2 |
| 2 | Freiheit | 2 | 2 |
| 3 | Umwelt | 1 | 1 |
| 4 | Demokratie | 1 | 1 |

View File

@ -0,0 +1,161 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import find from 'lodash/find'
import { eq } from 'semver';
/* 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) {
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)
})
}
Given('I am logged in', () => {
login('admin@example.org', 1234)
})
Given('we have a selection of tags and categories as well as posts', () => {
// TODO: use db factories instead of seed data
})
Given('my account has the following details:', table => {
// TODO: use db factories instead of seed data
})
Given('my user account has the role {string}', role => {
// TODO: use db factories instead of seed data
})
When('I log out', logout)
When('I visit the {string} page', page => {
openPage(page)
})
Given('I am on the {string} page', page => {
openPage(page)
})
When('I fill in my email and password combination and click submit', () => {
login('admin@example.org', 1234)
})
When('I refresh the page', () => {
cy.reload()
})
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')
.click()
})
Then('I can click on my profile picture in the top right corner', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
})
Then('I can see my name {string} in the dropdown menu', () => {
cy.get('.avatar-menu-popover').should('contain', username)
})
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', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover').contains(username)
})
When('I select {string} in the language menu', name => {
switchLanguage(name)
})
Given('I previously switched the language to {string}', name => {
switchLanguage(name)
})
Then('the whole user interface appears in {string}', name => {
const lang = getLangByName(name)
cy.get(`html[lang=${lang.code}]`)
cy.getCookie('locale').should('have.property', 'value', lang.code)
})
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()
})

20
cypress/plugins/index.js Normal file
View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const cucumber = require('cypress-cucumber-preprocessor').default
module.exports = on => {
// (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
on('file:preprocessor', cucumber())
}

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

20
cypress/support/index.js Normal file
View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,13 @@
version: '3.7'
services:
webapp:
volumes:
- .:/nitro-web
- node_modules:/nitro-web/node_modules
- node_modules_styleguide:/nitro-web/styleguide/node_modules
command: yarn run dev
volumes:
node_modules:
node_modules_styleguide:

View File

@ -2,17 +2,13 @@ version: '3.7'
services:
webapp:
build:
context: .
image: humanconnection/nitro-web:latest
build: .
ports:
- 3000:3000
- 8080:8080
networks:
- hc-network
volumes:
- .:/HC-WebApp
- node_modules:/HC-WebApp/node_modules
command: yarn run dev
environment:
- HOST=0.0.0.0
- BACKEND_URL=http://backend:4000

View File

@ -7,3 +7,11 @@
</ds-container>
</div>
</template>
<script>
import seo from '~/components/mixins/seo'
export default {
mixins: [seo]
}
</script>

View File

@ -8,46 +8,63 @@
>
<ds-logo />
</a>
<template v-if="isLoggedIn">
<div style="float: right">
<no-ssr>
<v-popover
:open.sync="isPopoverOpen"
:open-group="Math.random().toString()"
placement="bottom-end"
trigger="manual"
offset="10"
style="float: right"
>
<a
:href="$router.resolve({name: 'profile-slug', params: {slug: user.slug}}).href"
@click.prevent="toggleMenu()"
>
<ds-avatar
:image="user.avatar"
:name="user.name"
size="42"
/>
</a>
<div
slot="popover"
style="padding-top: .5rem; padding-bottom: .5rem;"
@mouseover="popoverMouseEnter"
@mouseleave="popoveMouseLeave"
>
Hallo {{ user.name }}
<ds-menu
:routes="routes"
style="margin-left: -15px; margin-right: -15px; padding-top: 1rem; padding-bottom: 1rem;"
@click.native="toggleMenu"
/>
<ds-space margin="xx-small" />
<nuxt-link :to="{ name: 'logout'}">
Logout
</nuxt-link>
</div>
</v-popover>
<locale-switch
class="topbar-locale-switch"
placement="bottom"
offset="24"
/>
</no-ssr>
</template>
<template v-if="isLoggedIn">
<no-ssr>
<dropdown class="avatar-menu">
<template
slot="default"
slot-scope="{toggleMenu}"
>
<a
class="avatar-menu-trigger"
:href="$router.resolve({name: 'profile-slug', params: {slug: user.slug}}).href"
@click.prevent="toggleMenu()"
>
<ds-avatar
:image="user.avatar"
:name="user.name"
size="42"
/>
</a>
</template>
<template
slot="popover"
slot-scope="{toggleMenu}"
>
<div class="avatar-menu-popover">
{{ $t('login.hello') }} <b>{{ user.name }}</b>
<ds-menu
:routes="routes"
:is-exact="isExact"
>
<ds-menu-item
slot="Navigation"
slot-scope="item"
:route="item.route"
:parents="item.parents"
@click.native="toggleMenu"
>
<ds-icon :name="item.route.icon" /> {{ item.route.name }}
</ds-menu-item>
</ds-menu>
<ds-space margin="xx-small" />
<nuxt-link :to="{ name: 'logout'}">
<ds-icon name="sign-out" /> {{ $t('login.logout') }}
</nuxt-link>
</div>
</template>
</dropdown>
</no-ssr>
</template>
</div>
</ds-container>
</div>
<ds-container>
@ -60,21 +77,21 @@
<script>
import { mapGetters } from 'vuex'
import { setTimeout } from 'timers'
let mouseEnterTimer = null
let mouseLeaveTimer = null
import LocaleSwitch from '~/components/LocaleSwitch'
import Dropdown from '~/components/Dropdown'
import seo from '~/components/mixins/seo'
export default {
data() {
return {
isPopoverOpen: false
}
components: {
Dropdown,
LocaleSwitch
},
mixins: [seo],
computed: {
...mapGetters({
user: 'auth/user',
isLoggedIn: 'auth/isLoggedIn',
isAdmin: 'auth/isLoggedIn'
isAdmin: 'auth/isAdmin'
}),
routes() {
if (!this.user.slug) {
@ -82,49 +99,58 @@ export default {
}
let routes = [
{
name: 'Mein Profil',
path: `/profile/${this.user.slug}`
name: this.$t('profile.name'),
path: `/profile/${this.user.slug}`,
icon: 'user'
},
{
name: 'Einstellungen',
path: `/settings`
name: this.$t('settings.name'),
path: `/settings`,
icon: 'cogs'
}
]
if (this.isAdmin) {
routes.push({
name: 'Systemverwaltung',
path: `/admin`
name: this.$t('admin.name'),
path: `/admin`,
icon: 'shield'
})
}
return routes
}
},
beforeDestroy() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
},
methods: {
toggleMenu() {
this.isPopoverOpen = !this.isPopoverOpen
},
popoverMouseEnter() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (!this.isPopoverOpen) {
mouseEnterTimer = setTimeout(() => {
this.isPopoverOpen = true
}, 500)
}
},
popoveMouseLeave() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (this.isPopoverOpen) {
mouseLeaveTimer = setTimeout(() => {
this.isPopoverOpen = false
}, 300)
}
isExact(url) {
return this.$route.path.indexOf(url) === 0
}
}
}
</script>
<style lang="scss">
.topbar-locale-switch {
display: inline-block;
top: 8px;
right: 10px;
position: relative;
}
.avatar-menu {
float: right;
}
.avatar-menu-trigger {
user-select: none;
}
.avatar-menu-popover {
display: inline-block;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
nav {
margin-left: -15px;
margin-right: -15px;
padding-top: 1rem;
padding-bottom: 1rem;
}
}
</style>

97
locales/de.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "Wenn Du ein Konto bei Human Connection hast, melde Dich bitte hier an.",
"login": "Einloggen",
"logout": "Ausloggen",
"email": "Deine E-Mail",
"password": "Dein Passwort",
"moreInfo": "Was ist Human Connection?",
"hello": "Hallo"
},
"profile": {
"name": "Mein Profil",
"memberSince": "Mitglied seit",
"follow": "Folgen",
"followers": "Folgen",
"following": "Folgt"
},
"settings": {
"name": "Einstellungen",
"data": {
"name": "Deine Daten"
},
"security": {
"name": "Sicherheit"
},
"invites": {
"name": "Einladungen"
},
"download": {
"name": "Daten herunterladen"
},
"delete": {
"name": "Konto löschen"
},
"organizations": {
"name": "Meine Organisationen"
},
"languages": {
"name": "Sprachen"
}
},
"admin": {
"name": "Systemverwaltung",
"dashboard": {
"name": "Startzentrale",
"users": "Benutzer",
"posts": "Beiträge",
"comments": "Kommentare",
"notifications": "Benachrichtigungen",
"organizations": "Organisationen",
"projects": "Projekte",
"invites": "Einladungen",
"follows": "Folgen",
"shouts": "Shouts"
},
"organizations": {
"name": "Organisationen"
},
"users": {
"name": "Benutzer"
},
"pages": {
"name": "Seiten"
},
"notifications": {
"name": "Benachrichtigungen"
},
"categories": {
"name": "Kategorien",
"categoryName": "Name",
"postCount": "Beiträge"
},
"tags": {
"name": "Schlagworte",
"tagCountUnique": "Benutzer",
"tagCount": "Beiträge"
},
"settings": {
"name": "Einstellungen"
}
},
"post": {
"name": "Beitrag",
"moreInfo": {
"name": "Mehr Info"
},
"takeAction": {
"name": "Aktiv werden"
}
},
"quotes": {
"african": {
"quote": "Viele kleine Leute, an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.",
"author": "Afrikanisches Sprichwort"
}
}
}

97
locales/en.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "If you already have a human-connection account, login here.",
"login": "Login",
"logout": "Logout",
"email": "Your Email",
"password": "Your Password",
"moreInfo": "What is Human Connection?",
"hello": "Hello"
},
"profile": {
"name": "My Profile",
"memberSince": "Member since",
"follow": "Follow",
"followers": "Followers",
"following": "Following"
},
"settings": {
"name": "Settings",
"data": {
"name": "Your data"
},
"security": {
"name": "Security"
},
"invites": {
"name": "Invites"
},
"download": {
"name": "Download Data"
},
"delete": {
"name": "Delete Account"
},
"organizations": {
"name": "My Organizations"
},
"languages": {
"name": "Languages"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Dashboard",
"users": "Users",
"posts": "Posts",
"comments": "Comments",
"notifications": "Notifications",
"organizations": "Organizations",
"projects": "Projects",
"invites": "Invites",
"follows": "Follows",
"shouts": "Shouts"
},
"organizations": {
"name": "Organizations"
},
"users": {
"name": "Users"
},
"pages": {
"name": "Pages"
},
"notifications": {
"name": "Notifications"
},
"categories": {
"name": "Categories",
"categoryName": "Name",
"postCount": "Posts"
},
"tags": {
"name": "Tags",
"tagCountUnique": "Users",
"tagCount": "Posts"
},
"settings": {
"name": "Settings"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "More info"
},
"takeAction": {
"name": "Take action"
}
},
"quotes": {
"african": {
"quote": "Many small people in many small places do many small things, that can alter the face of the world.",
"author": "African proverb"
}
}
}

79
locales/es.json Normal file
View File

@ -0,0 +1,79 @@
{
"login": {
"copy": "Si ya tiene una cuenta de Human Connection, inicie sesión aquí.",
"logout": "Cierre de sesión",
"email": "Tu correo electrónico",
"password": "Tu contraseña",
"moreInfo": "¿Qué es Human Connection?",
"hello": "Hola"
},
"profile": {
"name": "Mi perfil",
"memberSince": "Miembro desde",
"followers": "Seguidores"
},
"settings": {
"data": {
"name": "Sus datos"
},
"security": {
"name": "Seguridad"
},
"invites": {
"name": "Invita"
},
"download": {
"name": "Descargar datos"
},
"organizations": {
"name": "Mis organizaciones"
},
"languages": {
"name": "Idiomas"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"users": "Usuarios",
"comments": "Comentarios",
"organizations": "Organizaciones",
"projects": "Proyectos",
"invites": "Invita",
"follows": "Sigue"
},
"organizations": {
"name": "Organizaciones"
},
"users": {
"name": "Usuarios"
},
"pages": {
"name": "Páginas"
},
"notifications": {
"name": "Notificaciones"
},
"categories": {
"name": "Categorías",
"categoryName": "Nombre"
},
"tags": {
"name": "Etiquetas",
"tagCountUnique": "Usuarios"
}
},
"post": {
"moreInfo": {
"name": "Más info"
},
"takeAction": {
"name": "Tomar acción"
}
},
"quotes": {
"african": {
"author": "Proverbio africano"
}
}
}

97
locales/fr.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "Si vous avez déjà un compte human-connection, connectez-vous ici.",
"login": "Connexion",
"logout": "Déconnexion",
"email": "Votre Message électronique",
"password": "Votre mot de passe",
"moreInfo": "Qu'est-ce que Human Connection?",
"hello": "Bonjour"
},
"profile": {
"name": "Mon profil",
"memberSince": "Membre depuis",
"follow": "Suivre",
"followers": "Suiveurs",
"following": "Suivant"
},
"settings": {
"name": "Paramètres",
"data": {
"name": "Vos données"
},
"security": {
"name": "Sécurité"
},
"invites": {
"name": "Invite"
},
"download": {
"name": "Télécharger les données"
},
"delete": {
"name": "Supprimer un compte"
},
"organizations": {
"name": "Mes organisations"
},
"languages": {
"name": "Langues"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Tableau de bord",
"users": "Utilisateurs",
"posts": "Postes",
"comments": "Commentaires",
"notifications": "Notifications",
"organizations": "Organisations",
"projects": "Projets",
"invites": "Invite",
"follows": "Suit",
"shouts": "Cris"
},
"organizations": {
"name": "Organisations"
},
"users": {
"name": "Utilisateurs"
},
"pages": {
"name": "Pages"
},
"notifications": {
"name": "Notifications"
},
"categories": {
"name": "Catégories",
"categoryName": "Nom",
"postCount": "Postes"
},
"tags": {
"name": "Étiquettes",
"tagCountUnique": "Utilisateurs",
"tagCount": "Postes"
},
"settings": {
"name": "Paramètres"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "Plus d'infos"
},
"takeAction": {
"name": "Passez à l'action"
}
},
"quotes": {
"african": {
"quote": "Beaucoup de petites personnes dans beaucoup de petits endroits font beaucoup de petites choses, qui peuvent changer la face du monde.",
"author": "Proverbe africain"
}
}
}

50
locales/index.js Normal file
View File

@ -0,0 +1,50 @@
module.exports = [
{
name: 'English',
code: 'en',
iso: 'en-US',
enabled: true
},
{
name: 'Deutsch',
code: 'de',
iso: 'de-DE',
enabled: true
},
{
name: 'Nederlands',
code: 'nl',
iso: 'nl-NL',
enabled: true
},
{
name: 'Français',
code: 'fr',
iso: 'fr-FR',
enabled: true
},
{
name: 'Italiano',
code: 'it',
iso: 'it-IT',
enabled: true
},
{
name: 'Español',
code: 'es',
iso: 'es-ES',
enabled: true
},
{
name: 'Portuguese',
code: 'pt',
iso: 'pt-PT',
enabled: true
},
{
name: 'Polski',
code: 'pl',
iso: 'pl-PL',
enabled: true
}
]

89
locales/it.json Normal file
View File

@ -0,0 +1,89 @@
{
"login": {
"copy": "Se hai già un account di Human Connection, accedi qui.",
"logout": "Logout",
"email": "La tua email",
"password": "La tua password",
"moreInfo": "Che cosa è Human Connection?",
"hello": "Ciao"
},
"profile": {
"name": "Il mio profilo",
"follow": "Seguire",
"followers": "Seguaci"
},
"settings": {
"name": "Impostazioni",
"data": {
"name": "I tuoi dati"
},
"security": {
"name": "Sicurezza"
},
"invites": {
"name": "Inviti"
},
"download": {
"name": "Scaricare i dati"
},
"delete": {
"name": "Elimina Account"
},
"organizations": {
"name": "Mie organizzazioni"
},
"languages": {
"name": "Lingue"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Cruscotto",
"users": "Utenti",
"comments": "Commenti",
"notifications": "Notifiche",
"organizations": "Organizzazioni",
"projects": "Progetti",
"invites": "Inviti",
"follows": "Segue"
},
"organizations": {
"name": "Organizzazioni"
},
"users": {
"name": "Utenti"
},
"pages": {
"name": "Pagine"
},
"notifications": {
"name": "Notifiche"
},
"categories": {
"name": "Categorie",
"categoryName": "Nome"
},
"tags": {
"name": "Tag",
"tagCountUnique": "Utenti",
"tagCount": "Messaggi"
},
"settings": {
"name": "Impostazioni"
}
},
"post": {
"moreInfo": {
"name": "Ulteriori informazioni"
},
"takeAction": {
"name": "Agire"
}
},
"quotes": {
"african": {
"author": "Proverbio africano"
}
}
}

90
locales/nl.json Normal file
View File

@ -0,0 +1,90 @@
{
"login": {
"copy": "Als u al een mini-aansluiting account heeft, log dan hier in.",
"login": "Inloggen",
"logout": "Uitloggen",
"email": "Uw E-mail",
"password": "Uw Wachtwoord",
"moreInfo": "Wat is Human Connection?",
"hello": "Hallo"
},
"profile": {
"name": "Mijn profiel",
"memberSince": "Lid sinds",
"follow": "Volgen",
"followers": "Volgelingen",
"following": "Volgt"
},
"settings": {
"name": "Instellingen",
"data": {
"name": "Uw gegevens"
},
"security": {
"name": "Veiligheid"
},
"download": {
"name": "Gegevens downloaden"
},
"delete": {
"name": "Account verwijderen"
},
"organizations": {
"name": "Mijn Organisaties"
},
"languages": {
"name": "Talen"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Dashboard",
"users": "Gebruikers",
"posts": "Berichten",
"comments": "Opmerkingen",
"notifications": "Meldingen",
"organizations": "Organisaties",
"projects": "Projecten",
"follows": "Volgt",
"shouts": "Shouts"
},
"organizations": {
"name": "Organisaties"
},
"users": {
"name": "Gebruikers"
},
"notifications": {
"name": "Meldingen"
},
"categories": {
"name": "Categorieën",
"categoryName": "Naam",
"postCount": "Berichten"
},
"tags": {
"name": "Tags",
"tagCountUnique": "Gebruikers",
"tagCount": "Berichten"
},
"settings": {
"name": "Instellingen"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "Meer info"
},
"takeAction": {
"name": "Onderneem actie"
}
},
"quotes": {
"african": {
"quote": "Veel kleine mensen op veel kleine plaatsen doen veel kleine dingen, die het gezicht van de wereld kunnen veranderen.",
"author": "Afrikaans spreekwoord"
}
}
}

97
locales/pl.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.",
"login": "Login",
"logout": "Wyloguj się",
"email": "Twój adres e-mail",
"password": "Twoje hasło",
"moreInfo": "Co to jest Human Connection?",
"hello": "Cześć"
},
"profile": {
"name": "Mój profil",
"memberSince": "Członek od",
"follow": "Obserwuj",
"followers": "Obserwujący",
"following": "Obserwowani"
},
"settings": {
"name": "Ustawienia",
"data": {
"name": "Twoje dane"
},
"security": {
"name": "Bezpieczeństwo"
},
"invites": {
"name": "Zaproszenia"
},
"download": {
"name": "Pobierz dane"
},
"delete": {
"name": "Usuń konto"
},
"organizations": {
"name": "Moje organizacje"
},
"languages": {
"name": "Języki"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Tablica rozdzielcza",
"users": "Użytkownicy",
"posts": "Posty",
"comments": "Komentarze",
"notifications": "Powiadomienia",
"organizations": "Organizacje",
"projects": "Projekty",
"invites": "Zaproszenia",
"follows": "Obserwowań",
"shouts": "Wykrzyki"
},
"organizations": {
"name": "Organizacje"
},
"users": {
"name": "Użytkownicy"
},
"pages": {
"name": "Strony"
},
"notifications": {
"name": "Powiadomienia"
},
"categories": {
"name": "Kategorie",
"categoryName": "Nazwa",
"postCount": "Posty"
},
"tags": {
"name": "Tagi",
"tagCountUnique": "Użytkownicy",
"tagCount": "Posty"
},
"settings": {
"name": "Ustawienia"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "Więcej informacji"
},
"takeAction": {
"name": "Podejmij działanie"
}
},
"quotes": {
"african": {
"quote": "Wielu małych ludzi w wielu małych miejscach robi wiele małych rzeczy, które mogą zmienić oblicze świata.",
"author": "Afrykańskie przysłowie"
}
}
}

96
locales/pt.json Normal file
View File

@ -0,0 +1,96 @@
{
"login": {
"copy": "Se você já tem uma conta no Human Connection, faça o login aqui.",
"login": "Login",
"logout": "Sair",
"email": "Seu email",
"password": "Sua senha",
"moreInfo": "O que é o Human Connection?",
"hello": "Olá"
},
"profile": {
"name": "Meu perfil",
"memberSince": "Membro desde",
"follow": "Seguir",
"followers": "Seguidores",
"following": "Seguindo"
},
"settings": {
"name": "Configurações",
"data": {
"name": "Seus Dados"
},
"security": {
"name": "Segurança"
},
"invites": {
"name": "Convites"
},
"download": {
"name": "Baixar dados"
},
"delete": {
"name": "Deletar conta"
},
"organizations": {
"name": "Minhas Organizações"
},
"languages": {
"name": "Linguagens"
}
},
"admin": {
"name": "Administrator",
"dashboard": {
"name": "Painel de controle",
"users": "Usuários",
"posts": "Postagens",
"comments": "Comentários",
"notifications": "Notificações",
"organizations": "Organizações",
"projects": "Projetos",
"invites": "Convites",
"follows": "Seguidores"
},
"organizations": {
"name": "Organizações"
},
"users": {
"name": "Usuários"
},
"pages": {
"name": "Páginas"
},
"notifications": {
"name": "Notificações"
},
"categories": {
"name": "Categorias",
"categoryName": "Nome",
"postCount": "Postagens"
},
"tags": {
"name": "Etiquetas",
"tagCountUnique": "Usuários",
"tagCount": "Postagens"
},
"settings": {
"name": "Configurações"
}
},
"post": {
"name": "Postar",
"moreInfo": {
"name": "Mais informações"
},
"takeAction": {
"name": "Tomar uma ação"
}
},
"quotes": {
"african": {
"quote": "Pequenos grupos de pessoas, em pequenos locais podem fazer várias coisas pequenas, mas que podem alterar o mundo ao nosso redor.",
"author": "Provérbio Africano"
}
}
}

BIN
lokalise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,4 +1,4 @@
import { isEmpty } from 'lodash'
import isEmpty from 'lodash/isEmpty'
export default async ({ store, env, route, redirect }) => {
let publicPages = env.publicPages

View File

@ -26,7 +26,9 @@ module.exports = {
'pages-slug'
],
// pages to keep alive
keepAlivePages: ['index']
keepAlivePages: ['index'],
// active locales
locales: require('./locales')
},
/*
** Headers of the page
@ -59,6 +61,7 @@ module.exports = {
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~/plugins/i18n.js', ssr: true },
{ src: '~/plugins/keep-alive.js', ssr: false },
{ src: '~/plugins/design-system.js', ssr: true },
{ src: '~/plugins/vue-directives.js', ssr: false },
@ -71,30 +74,6 @@ module.exports = {
middleware: ['authenticated'],
linkActiveClass: 'router-active-link'
},
/* router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'post-slug',
path: '/post/:slug',
component: 'pages/post/_slug.vue',
children: [
{
path: 'more-info',
component: 'pages/post/_slug.vue'
},
{
path: 'take-action',
component: 'pages/post/_slug.vue'
}
]
}
]
}, */
/*
** Nuxt.js modules
@ -102,6 +81,7 @@ module.exports = {
modules: [
['@nuxtjs/dotenv', { only: envWhitelist }],
['nuxt-env', { keys: envWhitelist }],
'cookie-universal-nuxt',
'@nuxtjs/apollo',
'@nuxtjs/axios',
[
@ -172,6 +152,7 @@ module.exports = {
*/
build: {
/*
* TODO: import the polyfill instead of using the deprecated vendor key
* Polyfill missing ES6 & 7 Methods to work on older Browser
*/
vendor: ['@babel/polyfill'],

View File

@ -15,6 +15,9 @@
"test": "jest",
"precommit": "yarn lint"
},
"cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": true
},
"jest": {
"moduleFileExtensions": [
"js",
@ -29,15 +32,19 @@
"^@/(.*)$": "<rootDir>/src/$1",
"^@@/(.*)$": "<rootDir>/styleguide/src/system/$1",
"^~/(.*)$": "<rootDir>/$1"
}
},
"modulePathIgnorePatterns": [
"<rootDir>/styleguide"
]
},
"dependencies": {
"@nuxtjs/apollo": "^4.0.0-rc3",
"@nuxtjs/axios": "^5.3.6",
"@nuxtjs/dotenv": "^1.3.0",
"accounting": "^0.4.1",
"cookie-universal-nuxt": "^2.0.11",
"cross-env": "^5.2.0",
"date-fns": "^2.0.0-alpha.24",
"date-fns": "^2.0.0-alpha.26",
"express": "^4.16.3",
"graphql": "^14.0.2",
"graphql-tag": "^2.10.0",
@ -46,26 +53,29 @@
"nuxt-env": "^0.0.4",
"v-tooltip": "^2.0.0-rc.33",
"vue-count-to": "^1.0.13",
"vue-izitoast": "1.1.2"
"vue-izitoast": "1.1.2",
"vuex-i18n": "^1.10.5"
},
"devDependencies": {
"@vue/eslint-config-prettier": "^4.0.1",
"@vue/test-utils": "^1.0.0-beta.25",
"@vue/server-test-utils": "^1.0.0-beta.27",
"@vue/test-utils": "^1.0.0-beta.27",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-preset-env": "^1.7.0",
"eslint": "^5.0.1",
"cypress-cucumber-preprocessor": "^1.9.1",
"eslint": "^5.11.0",
"eslint-config-prettier": "^3.1.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-prettier": "3.0.0",
"eslint-plugin-vue": "^5.0.0",
"jest": "^23.6.0",
"node-sass": "^4.9.3",
"nodemon": "^1.11.0",
"node-sass": "^4.11.0",
"nodemon": "^1.18.9",
"nuxt-sass-resources-loader": "^2.0.5",
"prettier": "1.14.3",
"sass-loader": "^7.1.0",
"vue-jest": "^3.0.0",
"vue-jest": "^3.0.2",
"vue-svg-loader": "^0.11.0"
}
}

View File

@ -1,7 +1,7 @@
<template>
<div>
<ds-heading tag="h1">
Administartion
{{ $t('admin.name') }}
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }">
@ -21,39 +21,39 @@
<script>
export default {
data() {
return {
routes: [
computed: {
routes() {
return [
{
name: 'Dashboard',
name: this.$t('admin.dashboard.name'),
path: `/admin`
},
{
name: 'Users',
name: this.$t('admin.users.name'),
path: `/admin/users`
},
{
name: 'Organizations',
name: this.$t('admin.organizations.name'),
path: `/admin/organizations`
},
{
name: 'Pages',
name: this.$t('admin.pages.name'),
path: `/admin/pages`
},
{
name: 'Notifications',
name: this.$t('admin.notifications.name'),
path: `/admin/notifications`
},
{
name: 'Categories',
name: this.$t('admin.categories.name'),
path: `/admin/categories`
},
{
name: 'Tags',
name: this.$t('admin.tags.name'),
path: `/admin/tags`
},
{
name: 'Settings',
name: this.$t('admin.settings.name'),
path: `/admin/settings`
}
]

View File

@ -1,11 +1,11 @@
<template>
<ds-card space="small">
<ds-heading tag="h3">
Themen / Kategorien
{{ $t('admin.categories.name') }}
</ds-heading>
<ds-table
:data="Category"
:fields="['icon', 'name', 'postCount']"
:fields="fields"
condensed
>
<template
@ -27,6 +27,18 @@ export default {
Category: []
}
},
computed: {
fields() {
return {
icon: ' ',
name: this.$t('admin.categories.categoryName'),
postCount: {
label: this.$t('admin.categories.postCount'),
align: 'right'
}
}
}
},
apollo: {
Category: {
query: gql(`

View File

@ -7,7 +7,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Users"
:label="$t('admin.dashboard.users')"
size="x-large"
uppercase
>
@ -24,7 +24,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Posts"
:label="$t('admin.dashboard.posts')"
size="x-large"
uppercase
>
@ -41,7 +41,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Comments"
:label="$t('admin.dashboard.comments')"
size="x-large"
uppercase
>
@ -58,7 +58,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Notifications"
:label="$t('admin.dashboard.notifications')"
size="x-large"
uppercase
>
@ -75,7 +75,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Organization"
:label="$t('admin.dashboard.organizations')"
size="x-large"
uppercase
>
@ -92,7 +92,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Projects"
:label="$t('admin.dashboard.projects')"
size="x-large"
uppercase
>
@ -109,7 +109,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Open Invites"
:label="$t('admin.dashboard.invites')"
size="x-large"
uppercase
>
@ -126,7 +126,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Follows"
:label="$t('admin.dashboard.follows')"
size="x-large"
uppercase
>
@ -143,7 +143,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Shouts"
:label="$t('admin.dashboard.shouts')"
size="x-large"
uppercase
>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Notifications...
</ds-space>
<ds-card space="small">
<ds-heading tag="h3">
{{ $t('admin.notifications.name') }}
</ds-heading>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Organizations...
{{ $t('admin.organizations.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Pages...
{{ $t('admin.pages.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Settings...
{{ $t('admin.settings.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card space="small">
<ds-heading tag="h3">
Tags
{{ $t('admin.tags.name') }}
</ds-heading>
<ds-table
:data="Tag"
@ -24,12 +24,22 @@ import gql from 'graphql-tag'
export default {
data() {
return {
Tag: [],
fields: {
id: { label: '#' },
name: { label: 'Name' },
taggedCountUnique: { label: 'Nutzer' },
taggedCount: { label: 'Beiträge' }
Tag: []
}
},
computed: {
fields() {
return {
id: '#',
name: 'Name',
taggedCountUnique: {
label: this.$t('admin.tags.tagCountUnique'),
align: 'right'
},
taggedCount: {
label: this.$t('admin.tags.tagCount'),
align: 'right'
}
}
}
},

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Users...
{{ $t('admin.users.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,93 +1,112 @@
<template>
<ds-container width="small">
<ds-space margin="small">
<blockquote>
<p>
Viele kleine Leute, an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.
</p>
<b>- Afrikanisches Sprichwort</b>
</blockquote>
</ds-space>
<ds-card>
<ds-flex gutter="small">
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<ds-space
margin-top="small"
margin-bottom="xxx-small"
<transition
name="fade"
appear
>
<ds-container
v-if="ready"
width="small"
>
<ds-space margin="small">
<blockquote>
<p>{{ $t('quotes.african.quote') }}</p>
<b>- {{ $t('quotes.african.author') }}</b>
</blockquote>
</ds-space>
<ds-card class="login-card">
<ds-flex gutter="small">
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<img
class="login-image"
src="/img/sign-up/humanconnection.svg"
alt="Human Connection"
<no-ssr>
<locale-switch
class="login-locale-switch"
offset="5"
/>
</no-ssr>
<ds-space
margin-top="small"
margin-bottom="xxx-small"
center
>
</ds-space>
</ds-flex-item>
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<ds-space margin="small">
<ds-text size="small">
Wenn Du ein Konto bei Human Connection hast, melde Dich bitte hier an.
</ds-text>
</ds-space>
<form
:disabled="pending"
@submit.prevent="onSubmit"
>
<ds-input
v-model="form.email"
:disabled="pending"
placeholder="Deine E-Mail"
type="email"
name="email"
icon="envelope"
/>
<ds-input
v-model="form.password"
:disabled="pending"
placeholder="Dein Password"
icon="lock"
icon-right="question-circle"
name="password"
type="password"
/>
<ds-button
:loading="pending"
primary
full-width
name="submit"
type="submit"
>
Anmelden
</ds-button>
<ds-space margin="x-small">
<a
href="https://human-connection.org"
title="zur Präsentationsseite"
target="_blank"
<img
class="login-image"
src="/img/sign-up/humanconnection.svg"
alt="Human Connection"
>
Was ist Human Connection?
</a>
</ds-space>
</form>
</ds-flex-item>
</ds-flex>
</ds-card>
</ds-container>
</ds-flex-item>
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<ds-space margin="small">
<ds-text size="small">
{{ $t('login.copy') }}
</ds-text>
</ds-space>
<form
:disabled="pending"
@submit.prevent="onSubmit"
>
<ds-input
v-model="form.email"
:disabled="pending"
:placeholder="$t('login.email')"
type="email"
name="email"
icon="envelope"
/>
<ds-input
v-model="form.password"
:disabled="pending"
:placeholder="$t('login.password')"
icon="lock"
icon-right="question-circle"
name="password"
type="password"
/>
<ds-button
:loading="pending"
primary
full-width
name="submit"
type="submit"
icon="sign-in"
>
{{ $t('login.login') }}
</ds-button>
<ds-space margin="x-small">
<a
href="https://human-connection.org"
title="zur Präsentationsseite"
target="_blank"
>
{{ $t('login.moreInfo') }}
</a>
</ds-space>
</form>
</ds-flex-item>
</ds-flex>
</ds-card>
</ds-container>
</transition>
</template>
<script>
import LocaleSwitch from '~/components/LocaleSwitch'
import gql from 'graphql-tag'
export default {
components: {
LocaleSwitch
},
layout: 'blank',
data() {
return {
ready: false,
form: {
email: '',
password: ''
@ -104,6 +123,13 @@ export default {
return this.$store.getters['auth/pending']
}
},
mounted() {
setTimeout(() => {
// NOTE: quick fix for jumping flexbox implementation
// will be fixed in a future update of the styleguide
this.ready = true
}, 50)
},
methods: {
async onSubmit() {
try {
@ -123,4 +149,12 @@ export default {
width: 90%;
max-width: 200px;
}
.login-card {
position: relative;
}
.login-locale-switch {
position: absolute;
top: 1em;
left: 1em;
}
</style>

View File

@ -34,7 +34,10 @@
<!--<div class="tags">
<ds-icon name="compass" /> <ds-tag
v-for="category in post.categories"
:key="category.id"><ds-icon :name="category.icon" /> {{ category.name }}</ds-tag>
:key="category.id"
>
{{ category.name }}
</ds-tag>
</div>-->
<!-- Tags -->
<template v-if="post.tags && post.tags.length">

View File

@ -38,7 +38,7 @@
color="soft"
size="small"
>
Mitglied seit {{ user.createdAt | date('MMMM yyyy') }}
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
</ds-text>
</ds-space>
<ds-space
@ -52,7 +52,7 @@
<ds-flex>
<ds-flex-item>
<no-ssr>
<ds-number label="Folgen">
<ds-number :label="$t('profile.following')">
<hc-count-to
slot="count"
:end-val="followedByCount"
@ -62,7 +62,7 @@
</ds-flex-item>
<ds-flex-item>
<no-ssr>
<ds-number label="Folgt">
<ds-number :label="$t('profile.followers')">
<hc-count-to
slot="count"
:end-val="Number(user.followingCount) || 0"

View File

@ -1,7 +1,7 @@
<template>
<div>
<ds-heading tag="h1">
Settings
{{ $t('settings.name') }}
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }">
@ -21,36 +21,36 @@
<script>
export default {
data() {
return {
routes: [
computed: {
routes() {
return [
{
name: 'Your Data',
name: this.$t('settings.data.name'),
path: `/settings`
},
{
name: 'Password',
path: `/settings/password`
name: this.$t('settings.security.name'),
path: `/settings/security`
},
{
name: 'Invites',
name: this.$t('settings.invites.name'),
path: `/settings/invites`
},
{
name: 'Data Download',
name: this.$t('settings.download.name'),
path: `/settings/data-download`
},
{
name: 'Delete Account',
name: this.$t('settings.delete.name'),
path: `/settings/delete-account`
},
{
name: 'My Organizations',
name: this.$t('settings.organizations.name'),
path: `/settings/my-organizations`
},
{
name: 'Settings',
path: `/settings/settings`
name: this.$t('settings.languages.name'),
path: `/settings/languages`
}
]
}

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Download my Data...
{{ $t('settings.download.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Delete my Account...
{{ $t('settings.delete.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,5 +1,6 @@
<template>
<ds-card space="small">
{{ $t('settings.data.name') }}
<ds-input
id="name"
v-model="form.name"

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Invites...
{{ $t('settings.invites.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Settings...
{{ $t('settings.languages.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
My Organizations...
{{ $t('settings.organizations.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Change my Password...
{{ $t('settings.security.name') }}
</ds-space>
</ds-card>
</template>

View File

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

102
plugins/i18n.js Normal file
View File

@ -0,0 +1,102 @@
import Vue from 'vue'
import Vuex from 'vuex'
import vuexI18n from 'vuex-i18n/dist/vuex-i18n.umd.js'
import { debounce, isEmpty, find } from 'lodash'
/**
* TODO: Refactor and simplify browser detection
* and implement the user preference logic
*/
export default ({ app, req, cookie, store }) => {
const debug = app.$env.NODE_ENV !== 'production'
const key = 'locale'
const changeHandler = async mutation => {
if (process.server) return
const newLocale = mutation.payload.locale
const currentLocale = await app.$cookies.get(key)
const isDifferent = newLocale !== currentLocale
if (!isDifferent) {
return
}
app.$cookies.set(key, newLocale)
if (!app.$i18n.localeExists(newLocale)) {
import(`~/locales/${newLocale}.json`).then(res => {
app.$i18n.add(newLocale, res.default)
})
}
const user = store.getters['auth/user']
const token = store.getters['auth/token']
// persist language if it differs from last value
if (user && user._id && token) {
// TODO: SAVE LOCALE
// store.dispatch('usersettings/patch', {
// uiLanguage: newLocale
// }, { root: true })
}
}
// const i18nStore = new Vuex.Store({
// strict: debug
// })
Vue.use(vuexI18n.plugin, store, {
onTranslationNotFound: function(locale, key) {
if (debug) {
console.warn(
`vuex-i18n :: Key '${key}' not found for locale '${locale}'`
)
}
}
})
// register the fallback locales
Vue.i18n.add('en', require('~/locales/en.json'))
let userLocale = 'en'
const localeCookie = app.$cookies.get(key)
/* const userSettings = store.getters['auth/userSettings']
if (userSettings && userSettings.uiLanguage) {
// try to get saved user preference
userLocale = userSettings.uiLanguage
} else */
if (!isEmpty(localeCookie)) {
userLocale = localeCookie
} else {
userLocale = process.browser
? navigator.language || navigator.userLanguage
: req.locale
if (userLocale && !isEmpty(userLocale.language)) {
userLocale = userLocale.language.substr(0, 2)
}
}
const availableLocales = process.env.locales.filter(lang => !!lang.enabled)
const locale = find(availableLocales, ['code', userLocale])
? userLocale
: 'en'
if (locale !== 'en') {
Vue.i18n.add(locale, require(`~/locales/${locale}.json`))
}
// Set the start locale to use
Vue.i18n.set(locale)
Vue.i18n.fallback('en')
if (process.browser) {
store.subscribe(mutation => {
if (mutation.type === 'i18n/SET_LOCALE') {
changeHandler(mutation)
}
})
}
app.$i18n = Vue.i18n
return store
}

View File

@ -1,25 +1,44 @@
import Vue from 'vue'
import { en, de } from 'date-fns/locale'
import { enUS, de, nl, fr, es } from 'date-fns/locale'
import format from 'date-fns/format'
import formatRelative from 'date-fns/formatRelative'
import addSeconds from 'date-fns/addSeconds'
import accounting from 'accounting'
export default ({ app }) => {
const locales = {
en: enUS,
de: de,
nl: nl,
fr: fr,
es: es,
pt: es,
pl: de
}
const getLocalizedFormat = () => {
let locale = app.$i18n.locale()
locale = locales[locale] ? locale : 'en'
return locales[locale]
}
app.$filters = Object.assign(app.$filters || {}, {
date: (value, fmt = 'dd. MMM yyyy') => {
if (!value) return ''
return format(new Date(value), fmt, { locale: de })
return format(new Date(value), fmt, {
locale: getLocalizedFormat()
})
},
dateTime: (value, fmt = 'dd. MMM yyyy HH:mm') => {
if (!value) return ''
return format(new Date(value), fmt, { locale: de })
return format(new Date(value), fmt, {
locale: getLocalizedFormat()
})
},
relativeDateTime: value => {
if (!value) return ''
return formatRelative(new Date(value), new Date(), { locale: de })
return formatRelative(new Date(value), new Date(), {
locale: getLocalizedFormat()
})
},
number: (
value,

3
scripts/docker_push.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker push humanconnection/nitro-web:latest

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M16 345a256 256 0 0 0 480 0l-240-22.2L16 345z" fill="#ffda44"/><path d="M256 0A256 256 0 0 0 16 167l240 22.2L496 167A256 256 0 0 0 256 0z"/><path d="M16 167a255.5 255.5 0 0 0 0 178h480a255.4 255.4 0 0 0 0-178H16z" fill="#d80027"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><defs><path id="a" d="M0 512L512 0v512z"/></defs><g fill="none" fill-rule="evenodd"><circle cx="256" cy="256" r="256" fill="#F0F0F0"/><path fill="#D80027" fill-rule="nonzero" d="M327.7 189.2H245V256h15.6l67.2-66.8zm146.7-66.8a257.3 257.3 0 0 0-59-66.7H244.9v66.7h229.5zM127.8 389.6l67.4-66.8H8.8c6.4 23.5 16 46 28.8 66.8h90.2z"/><path fill="#0052B4" fill-rule="nonzero" d="M118.6 40h23.3l-21.7 15.7 8.3 25.6-21.7-15.8-21.7 15.8 7.2-22a257.4 257.4 0 0 0-49.7 55.3h7.5l-13.8 10c-2.2 3.6-4.2 7.2-6.2 11l6.6 20.2-12.3-9c-3.1 6.6-5.9 13.2-8.4 20l7.3 22.3H50L28.4 205l8.3 25.5L15 214.6l-13 9.5C.7 234.7 0 245.3 0 256h256V0c-50.6 0-97.7 14.7-137.4 40zm9.9 190.4l-21.7-15.8-21.7 15.8 8.3-25.5L71.7 189h26.8l8.3-25.5 8.3 25.5h26.8L120.2 205l8.3 25.5zm-8.3-100l8.3 25.4-21.7-15.7-21.7 15.7 8.3-25.5-21.7-15.7h26.8l8.3-25.6 8.3 25.6h26.8l-21.7 15.7zm100.1 100l-21.7-15.8-21.7 15.8 8.3-25.5-21.7-15.8h26.8l8.3-25.5 8.3 25.5h26.8L212 205l8.3 25.5zm-8.3-100l8.3 25.4-21.7-15.7-21.7 15.7 8.3-25.5-21.7-15.7h26.8l8.3-25.6 8.3 25.6h26.8L212 130.3zm0-74.7l8.3 25.6-21.7-15.8L177 81.3l8.3-25.6L163.5 40h26.8l8.3-25.5L207 40h26.8L212 55.7z"/><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><circle cx="256" cy="256" r="256" fill="#F0F0F0"/><path fill="#0052B4" fill-rule="nonzero" d="M503.2 189.2a255 255 0 0 0-44.1-89l-89.1 89h133.2zm-403 269.9a255 255 0 0 0 89 44V370l-89 89zm222.6 44a255 255 0 0 0 89-44l-89-89.1v133.2zM370 322.9l89 89a255 255 0 0 0 44.2-89H370z"/><path fill="#D80027" d="M509.8 222.6H222.4l.2 287.2c22.2 3 44.6 3 66.8 0V289.4h220.4a258.5 258.5 0 0 0 0-66.8z"/><path fill="#D80027" fill-rule="nonzero" d="M322.8 322.8L437 437c5.3-5.2 10.3-10.7 15-16.4l-97.7-97.8h-31.5zm-133.6 0L75 437c5.2 5.3 10.7 10.3 16.4 15l97.8-97.7v-31.5z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 256c0 31.3 5.6 61.3 16 89l240 22.3L496 345a255.5 255.5 0 0 0 0-178l-240-22.3L16 167a255.5 255.5 0 0 0-16 89z" fill="#ffda44"/><path d="M496 167a256 256 0 0 0-480 0h480zM16 345a256 256 0 0 0 480 0H16z" fill="#d80027"/></svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M512 256A256 256 0 0 0 345 16v480a256 256 0 0 0 167-240z" fill="#d80027"/><path d="M0 256a256 256 0 0 0 167 240V16A256 256 0 0 0 0 256z" fill="#0052b4"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M512 256A256 256 0 0 0 345 16v480a256 256 0 0 0 167-240z" fill="#d80027"/><path d="M0 256a256 256 0 0 0 167 240V16A256 256 0 0 0 0 256z" fill="#6da544"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M256 0A256 256 0 0 0 16 167h480A256 256 0 0 0 256 0z" fill="#a2001d"/><path d="M256 512a256 256 0 0 0 240-167H16a256 256 0 0 0 240 167z" fill="#0052b4"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M512 256a256 256 0 0 1-512 0" fill="#d80027"/></svg>

After

Width:  |  Height:  |  Size: 173 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 256a256 256 0 0 0 167 240l22.2-240L167 16A256 256 0 0 0 0 256z" fill="#6da544"/><path d="M512 256A256 256 0 0 0 167 16v480a256 256 0 0 0 345-240z" fill="#d80027"/><circle cx="167" cy="256" r="89" fill="#ffda44"/><path d="M116.9 211.5V267a50 50 0 1 0 100.1 0v-55.6H117z" fill="#d80027"/><path d="M167 283.8c-9.2 0-16.7-7.5-16.7-16.7V245h33.4V267c0 9.2-7.5 16.7-16.7 16.7z" fill="#f0f0f0"/></svg>

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -13,11 +13,6 @@ export const mutations = {
SET_USER(state, user) {
state.user = user || null
},
SET_USER_SETTINGS(state, userSettings) {
// state.user = Object.assign(state.user, {
// userSettings: Object.assign(this.getters['auth/userSettings'], userSettings)
// })
},
SET_TOKEN(state, token) {
state.token = token || null
},
@ -27,22 +22,22 @@ export const mutations = {
}
export const getters = {
isAuthenticated(state) {
return !!state.token
},
isLoggedIn(state) {
return !!(state.user && state.token)
},
pending(state) {
return !!state.pending
},
isVerified(state) {
return !!state.user && state.user.isVerified && !!state.user.name
},
isAdmin(state) {
return !!state.user && state.user.role === 'ADMIN'
return !!state.user && state.user.role === 'admin'
},
isModerator(state) {
return (
!!state.user &&
(state.user.role === 'ADMIN' || state.user.role === 'MODERATOR')
(state.user.role === 'admin' || state.user.role === 'moderator')
)
},
user(state) {
@ -51,20 +46,6 @@ export const getters = {
token(state) {
return state.token
}
// userSettings(state, getters, rootState, rootGetters) {
// const userSettings = (state.user && state.user.userSettings) ? state.user.userSettings : {}
//
// const defaultLanguage = (state.user && state.user.language) ? state.user.language : rootGetters['i18n/locale']
// let contentLanguages = !isEmpty(userSettings.contentLanguages) ? userSettings.contentLanguages : []
// if (isEmpty(contentLanguages)) {
// contentLanguages = userSettings.uiLanguage ? [userSettings.uiLanguage] : [defaultLanguage]
// }
//
// return Object.assign({
// uiLanguage: defaultLanguage,
// contentLanguages: contentLanguages
// }, userSettings)
// }
}
export const actions = {
@ -114,10 +95,8 @@ export const actions = {
})
},
async login({ commit }, { email, password }) {
commit('SET_PENDING', true)
try {
commit('SET_PENDING', true)
commit('SET_USER', null)
commit('SET_TOKEN', null)
const res = await this.app.apolloProvider.defaultClient
.mutate({
mutation: gql(`
@ -139,22 +118,15 @@ export const actions = {
})
.then(({ data }) => data && data.login)
if (res && res.token) {
await this.app.$apolloHelpers.onLogin(res.token)
commit('SET_TOKEN', res.token)
delete res.token
commit('SET_USER', res)
commit('SET_PENDING', false)
return true
} else {
commit('SET_PENDING', false)
throw new Error('THERE IS AN ERROR')
}
await this.app.$apolloHelpers.onLogin(res.token)
commit('SET_TOKEN', res.token)
const userData = Object.assign({}, res)
delete userData.token
commit('SET_USER', userData)
} catch (err) {
commit('SET_USER', null)
commit('SET_TOKEN', null)
commit('SET_PENDING', false)
throw new Error(err)
} finally {
commit('SET_PENDING', false)
}
},
async logout({ commit }) {

160
store/auth.test.js Normal file
View File

@ -0,0 +1,160 @@
import { getters, mutations, actions } from './auth.js'
let state
let commit
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwic2x1ZyI6Implbm55LXJvc3RvY2siLCJuYW1lIjoiSmVubnkgUm9zdG9jayIsImF2YXRhciI6Imh0dHBzOi8vczMuYW1hem9uYXdzLmNvbS91aWZhY2VzL2ZhY2VzL3R3aXR0ZXIvbXV0dV9rcmlzaC8xMjguanBnIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUub3JnIiwicm9sZSI6InVzZXIiLCJpYXQiOjE1NDUxNDQ2ODgsImV4cCI6MTYzMTU0NDY4OCwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwIiwic3ViIjoidTMifQ.s5_JeQN9TaUPfymAXPOpbMAwhmTIg9cnOvNEcj4z75k'
const successfulLoginResponse = {
data: {
login: {
id: 'u3',
name: 'Jenny Rostock',
slug: 'jenny-rostock',
email: 'user@example.org',
avatar:
'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg',
role: 'user',
token
}
}
}
const incorrectPasswordResponse = {
data: {
login: null
},
errors: [
{
message: 'Incorrect password.',
locations: [
{
line: 2,
column: 3
}
],
path: ['login']
}
]
}
beforeEach(() => {
commit = jest.fn()
})
describe('getters', () => {
describe('isAuthenticated', () => {
describe('given JWT Bearer token', () => {
test('true', () => {
state = { token }
expect(getters.isAuthenticated(state)).toBe(true)
})
})
})
})
describe('actions', () => {
let action
describe('login', () => {
describe('given valid credentials and a successful response', () => {
beforeEach(async () => {
const response = Object.assign({}, successfulLoginResponse)
const mutate = jest.fn(() => Promise.resolve(response))
const onLogin = jest.fn(() => Promise.resolve())
const module = {
app: {
apolloProvider: { defaultClient: { mutate } },
$apolloHelpers: { onLogin }
}
}
action = actions.login.bind(module)
await action(
{ commit },
{ email: 'user@example.org', password: '1234' }
)
})
afterEach(() => {
action = null
})
it('saves the JWT Bearer token', () => {
expect(commit.mock.calls).toEqual(
expect.arrayContaining([['SET_TOKEN', token]])
)
})
it('saves user data without token', () => {
expect(commit.mock.calls).toEqual(
expect.arrayContaining([
[
'SET_USER',
{
id: 'u3',
name: 'Jenny Rostock',
slug: 'jenny-rostock',
email: 'user@example.org',
avatar:
'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg',
role: 'user'
}
]
])
)
})
it('saves pending flags in order', () => {
expect(commit.mock.calls).toEqual(
expect.arrayContaining([
['SET_PENDING', true],
['SET_PENDING', false]
])
)
})
})
describe('given invalid credentials and incorrect password response', () => {
let onLogin
let mutate
beforeEach(() => {
mutate = jest.fn(() => Promise.reject('This error is expected.'))
onLogin = jest.fn(() => Promise.resolve())
const module = {
app: {
apolloProvider: { defaultClient: { mutate } },
$apolloHelpers: { onLogin }
}
}
action = actions.login.bind(module)
})
afterEach(() => {
action = null
})
it('populates error messages', async () => {
expect(
action({ commit }, { email: 'user@example.org', password: 'wrong' })
).rejects.toThrowError('This error is expected.')
expect(mutate).toHaveBeenCalled()
expect(onLogin).not.toHaveBeenCalled()
})
it('saves pending flags in order', async () => {
try {
await action(
{ commit },
{ email: 'user@example.org', password: 'wrong' }
)
} catch (err) {} // ignore
expect(commit.mock.calls).toEqual(
expect.arrayContaining([
['SET_PENDING', true],
['SET_PENDING', false]
])
)
})
})
})
})

View File

@ -20,7 +20,8 @@
<tr>
<ds-table-head-col
v-for="header in headers"
:key="header.key">
:key="header.key"
:align="align(header.key)">
{{ header.label }}
</ds-table-head-col>
</tr>
@ -31,7 +32,8 @@
:key="row.key || index">
<ds-table-col
v-for="col in row"
:key="col.key">
:key="col.key"
:align="align(col.key)">
<!-- @slot Slots are named by fields -->
<slot
:name="col.key"
@ -152,6 +154,9 @@ export default {
}
},
methods: {
align(colKey) {
return this.fields && this.fields[colKey] ? this.fields[colKey].align : null
},
parseLabel(label) {
return startCase(label)
}

View File

@ -1,5 +1,10 @@
<template>
<td class="ds-table-col">
<!-- eslint-disable -->
<td
class="ds-table-col"
:class="[
align && `ds-table-col-${align}`
]">
<slot/>
</td>
</template>
@ -25,6 +30,17 @@ export default {
width: {
type: [String, Number, Object],
default: null
},
/**
* The column align
* `left, center, right`
*/
align: {
type: String,
default: null,
validator: value => {
return value.match(/(left|center|right)/)
}
}
},
computed: {}

View File

@ -1,5 +1,9 @@
<template>
<th class="ds-table-head-col">
<th
class="ds-table-head-col"
:class="[
align && `ds-table-head-col-${align}`
]">
<slot>
{{ label }}
</slot>
@ -36,6 +40,17 @@ export default {
width: {
type: [String, Number, Object],
default: null
},
/**
* The column align
* `left, center, right`
*/
align: {
type: String,
default: null,
validator: value => {
return value.match(/(left|center|right)/)
}
}
},
computed: {}

View File

@ -100,7 +100,8 @@ The value can be a string representing the fields label or an object with option
name: 'Hero',
type: {
label: 'Job',
width: '300px'
width: '300px',
align: 'right'
}
},
tableData: [
@ -191,4 +192,4 @@ Via scoped slots you have access to the columns `row`, `index` and `col`.
}
}
</script>
```
```

View File

@ -42,3 +42,19 @@
padding-bottom: $space-x-small;
}
}
.ds-table-col,
.ds-table-head-col {
&.ds-table-col-left,
&.ds-table-head-col-left {
text-align: left;
}
&.ds-table-col-center,
&.ds-table-head-col-center {
text-align: center;
}
&.ds-table-col-right,
&.ds-table-head-col-right {
text-align: right;
}
}

View File

@ -46,7 +46,10 @@ export default {
default: null
},
align: {
/**
* Center content vertacally and horizontally
*/
center: {
type: Boolean,
default: false
},

View File

@ -44,6 +44,7 @@ ul.ds-menu-list {
text-decoration: none;
padding: $space-x-small $space-small;
transition: color $duration-short $ease-out;
border-left: 2px solid transparent;
&.router-link-active,
&.nuxt-link-active {
@ -56,8 +57,9 @@ ul.ds-menu-list {
&.router-link-exact-active,
&.nuxt-link-exact-active {
color: $text-color-link-active;
background-color: $background-color-soft;
color: $text-color-link;
// background-color: $background-color-soft;
border-left: 2px solid $color-primary;
}
.ds-menu-item-inverse & {

1235
yarn.lock

File diff suppressed because it is too large Load Diff