Merge branch 'master' into 37-full-text-search-top-bar

This commit is contained in:
mattwr18 2019-03-01 07:08:11 -03:00 committed by GitHub
commit 09a022a338
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1139 changed files with 1839 additions and 30166 deletions

View File

@ -1,6 +1,6 @@
.vscode/
styleguide/node_modules/
styleguide/
node_modules/
npm-debug.log

View File

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

2
.gitignore vendored
View File

@ -30,6 +30,7 @@ build/Release
# Dependency directories
node_modules/
styleguide/
# TypeScript v1 declaration files
typings/
@ -79,3 +80,4 @@ static/uploads
cypress/videos
cypress/screenshots/
cypress.env.json

View File

@ -18,22 +18,23 @@ before_install:
- 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
- cp cypress.env.template.json cypress.env.json
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 $BACKEND_BRANCH || git -C "../Nitro-Backend" checkout master
- docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.travis.yml up -d
- cd ../Nitro-Backend && yarn install && cd -
- docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.cypress.yml up -d
- yarn global add cypress wait-on
- yarn add cypress-cucumber-preprocessor
script:
- docker-compose exec -e NODE_ENV=test webapp yarn run lint
- docker-compose exec -e NODE_ENV=test webapp yarn run test
- docker-compose -f ../Nitro-Backend/docker-compose.yml exec backend yarn run db:seed
- wait-on http://localhost:3000
- cypress run --record --key $CYPRESS_TOKEN
- wait-on http://localhost:7474 && docker-compose -f ../Nitro-Backend/docker-compose.yml exec neo4j migrate
- 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

View File

@ -19,9 +19,6 @@ 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 .. \
&& yarn run styleguide:build
RUN yarn run build
FROM base as production

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Human-Connection gGmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,5 +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)
<p align="center">
<a href="https://human-connection.org"><img align="center" src="static/img/sign-up/humanconnection.png" height="200" alt="Human Connection" /></a>
</p>
# NITRO Web
[![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Web/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Web)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Web/blob/master/LICENSE.md)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web?ref=badge_shield)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3)
![UI Screenshot](screenshot.png)
@ -10,16 +17,15 @@
### Install
``` bash
# install all dependencies
$ cd styleguide && yarn install && cd ..
$ yarn styleguide:build
$ yarn install
```
Copy:
```
cp .env.template .env
cp cypress.env.template.json cypress.env.json
```
Configure the file `.env` according to your needs and your local setup.
Configure the files according to your needs and your local setup.
### Development
``` bash
@ -30,7 +36,6 @@ $ yarn dev
### Build for production
``` bash
# build for production and launch server
$ yarn styleguide:build
$ yarn build
$ yarn start
```
@ -41,10 +46,8 @@ All reusable Components (for example avatar) should be done inside the styleguid
![Styleguide Screenshot](screenshot-styleguide.png)
### To show the styleguide
``` bash
$ yarn styleguide
```
More information can be found here: https://github.com/Human-Connection/Nitro-Styleguide
## Internationalization (i18n)
@ -57,3 +60,6 @@ Thanks lokalise.co that we can use your premium account!
## 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>
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web?ref=badge_large)

View File

@ -78,45 +78,6 @@ blockquote {
}
}
.hc-editor-content {
h1,
h2,
h3,
h4,
h5,
h6 {
&:not(:first-child) {
margin-top: 2rem;
}
}
p {
&:not(:last-child) {
margin-top: 0;
margin-bottom: 0;
}
}
dl,
ol,
ul,
blockquote,
pre,
table {
&:not(:first-child) {
margin-top: 15px;
}
}
*:first-child {
margin-top: 0;
}
*:last-child {
margin-bottom: 0;
}
// avoid double breaks
br + p {
margin-top: 0;
}
}
hr {
border: 0;
width: 100%;

View File

@ -8,14 +8,19 @@
slot="default"
slot-scope="{toggleMenu}"
>
<ds-button
class="content-menu-trigger"
size="small"
ghost
@click.prevent="toggleMenu"
<slot
name="button"
:toggleMenu="toggleMenu"
>
<ds-icon name="ellipsis-v" />
</ds-button>
<ds-button
class="content-menu-trigger"
size="small"
ghost
@click.prevent="toggleMenu"
>
<ds-icon name="ellipsis-v" />
</ds-button>
</slot>
</template>
<div
slot="popover"
@ -49,6 +54,7 @@ export default {
placement: { type: String, default: 'top-end' },
itemId: { type: String, required: true },
name: { type: String, required: true },
isOwner: { type: Boolean, default: false },
context: {
type: String,
required: true,
@ -59,20 +65,54 @@ export default {
},
computed: {
routes() {
let routes = [
{
let routes = []
if (this.isOwner && this.context === 'contribution') {
routes.push({
name: this.$t(`contribution.edit`),
path: this.$router.resolve({
name: 'post-edit-id',
params: {
id: this.itemId
}
}).href,
icon: 'edit'
})
}
if (this.isOwner && this.context === 'comment') {
routes.push({
name: this.$t(`comment.edit`),
callback: () => {
console.log('EDIT COMMENT')
},
icon: 'edit'
})
}
if (!this.isOwner) {
routes.push({
name: this.$t(`report.${this.context}.title`),
callback: this.openReportDialog,
icon: 'flag'
}
]
if (this.isModerator) {
})
}
if (!this.isOwner && this.isModerator) {
routes.push({
name: this.$t(`disable.${this.context}.title`),
callback: this.openDisableDialog,
callback: () => {},
icon: 'eye-slash'
})
}
if (this.isOwner && this.context === 'user') {
routes.push({
name: this.$t(`settings.data.name`),
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
callback: () => this.$router.push('/settings'),
icon: 'edit'
})
}
return routes
},
isModerator() {

View File

@ -0,0 +1,145 @@
<template>
<ds-form
ref="contributionForm"
v-model="form"
:schema="formSchema"
@submit="submit"
>
<template slot-scope="{ errors }">
<ds-card>
<ds-input
model="title"
class="post-title"
placeholder="Title"
name="title"
autofocus
/>
<no-ssr>
<hc-editor
:value="form.content"
@input="updateEditorContent"
/>
</no-ssr>
<div
slot="footer"
style="text-align: right"
>
<ds-button
:disabled="loading || disabled"
ghost
@click.prevent="$router.back()"
>
{{ $t('actions.cancel') }}
</ds-button>
<ds-button
icon="check"
type="submit"
:loading="loading"
:disabled="disabled || errors"
primary
>
{{ $t('actions.save') }}
</ds-button>
</div>
</ds-card>
</template>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
import HcEditor from '~/components/Editor/Editor.vue'
export default {
components: {
HcEditor
},
props: {
contribution: { type: Object, default: () => {} }
},
data() {
return {
form: {
title: '',
content: ''
},
formSchema: {
title: { required: true, min: 3, max: 64 },
content: { required: true, min: 3 }
},
id: null,
loading: false,
disabled: false,
slug: null
}
},
watch: {
contribution: {
immediate: true,
handler: function(contribution) {
if (!contribution || !contribution.id) {
return
}
this.id = contribution.id
this.slug = contribution.slug
this.form.content = contribution.content
this.form.title = contribution.title
}
}
},
methods: {
submit() {
const postMutations = require('~/graphql/PostMutations.js').default(this)
this.loading = true
this.$apollo
.mutate({
mutation: this.id
? postMutations.UpdatePost
: postMutations.CreatePost,
variables: {
id: this.id,
title: this.form.title,
content: this.form.content
}
})
.then(res => {
this.loading = false
this.$toast.success('Saved!')
this.disabled = true
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
this.$router.push({
name: 'post-slug',
params: { slug: result.slug }
})
})
.catch(err => {
this.$toast.error(err.message)
this.loading = false
this.disabled = false
})
},
updateEditorContent(value) {
// this.form.content = value
this.$refs.contributionForm.update('content', value)
}
}
}
</script>
<style lang="scss">
.post-title {
margin-top: $space-x-small;
margin-bottom: $space-xx-small;
input {
border: 0;
font-size: $font-size-x-large;
font-weight: bold;
padding-left: 0;
padding-right: 0;
}
}
</style>

View File

@ -0,0 +1,366 @@
<template>
<div class="editor">
<editor-menu-bubble :editor="editor">
<div
ref="menu"
slot-scope="{ commands, getMarkAttrs, isActive, menu }"
class="menububble tooltip"
x-placement="top"
:class="{ 'is-active': menu.isActive || linkMenuIsActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
>
<div class="tooltip-wrapper">
<template v-if="linkMenuIsActive">
<ds-input
ref="linkInput"
v-model="linkUrl"
class="editor-menu-link-input"
placeholder="http://"
@blur.native.capture="hideMenu(menu.isActive)"
@keydown.native.esc.prevent="hideMenu(menu.isActive)"
@keydown.native.enter.prevent="setLinkUrl(commands.link, linkUrl)"
/>
</template>
<template v-else>
<ds-button
class="menububble__button"
size="small"
:hover="isActive.bold()"
ghost
@click.prevent="() => {}"
@mousedown.native.prevent="commands.bold"
>
<ds-icon name="bold" />
</ds-button>
<ds-button
class="menububble__button"
size="small"
:hover="isActive.italic()"
ghost
@click.prevent="() => {}"
@mousedown.native.prevent="commands.italic"
>
<ds-icon name="italic" />
</ds-button>
<ds-button
class="menububble__button"
size="small"
:hover="isActive.link()"
ghost
@click.prevent="() => {}"
@mousedown.native.prevent="showLinkMenu(getMarkAttrs('link'))"
>
<ds-icon name="link" />
</ds-button>
</template>
</div>
<div class="tooltip-arrow" />
</div>
</editor-menu-bubble>
<editor-floating-menu :editor="editor">
<div
slot-scope="{ commands, isActive, menu }"
class="editor__floating-menu"
:class="{ 'is-active': menu.isActive }"
:style="`top: ${menu.top}px`"
>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.paragraph()"
@click.prevent="commands.paragraph()"
>
<ds-icon name="paragraph" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.heading({ level: 3 })"
@click.prevent="commands.heading({ level: 3 })"
>
H3
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.heading({ level: 4 })"
@click.prevent="commands.heading({ level: 4 })"
>
H4
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.bullet_list()"
@click.prevent="commands.bullet_list()"
>
<ds-icon name="list-ul" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.ordered_list()"
@click.prevent="commands.ordered_list()"
>
<ds-icon name="list-ol" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.blockquote()"
@click.prevent="commands.blockquote"
>
<ds-icon name="quote-right" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.horizontal_rule()"
@click.prevent="commands.horizontal_rule"
>
<ds-icon name="minus" />
</ds-button>
</div>
</editor-floating-menu>
<editor-content :editor="editor" />
</div>
</template>
<script>
import linkify from 'linkify-it'
import stringHash from 'string-hash'
import {
Editor,
EditorContent,
EditorFloatingMenu,
EditorMenuBubble
} from 'tiptap'
import EventHandler from './plugins/eventHandler.js'
import {
Heading,
HardBreak,
Blockquote,
ListItem,
BulletList,
OrderedList,
HorizontalRule,
Placeholder,
Bold,
Italic,
Strike,
Underline,
Link,
History
} from 'tiptap-extensions'
let throttleInputEvent
export default {
components: {
EditorContent,
EditorFloatingMenu,
EditorMenuBubble
},
props: {
value: { type: String, default: '' },
doc: { type: Object, default: () => {} }
},
data() {
return {
lastValueHash: null,
editor: new Editor({
content: this.value || '',
doc: this.doc,
extensions: [
new EventHandler(),
new Heading(),
new HardBreak(),
new Blockquote(),
new BulletList(),
new OrderedList(),
new HorizontalRule(),
new Bold(),
new Italic(),
new Strike(),
new Underline(),
new Link(),
new Heading({ levels: [3, 4] }),
new ListItem(),
new Placeholder({
emptyNodeClass: 'is-empty',
emptyNodeText: 'Schreib etwas inspirerendes…'
}),
new History()
],
onUpdate: e => {
clearTimeout(throttleInputEvent)
throttleInputEvent = setTimeout(() => this.onUpdate(e), 300)
}
}),
linkUrl: null,
linkMenuIsActive: false
}
},
watch: {
value: {
immediate: true,
handler: function(content, old) {
const contentHash = stringHash(content)
if (!content || contentHash === this.lastValueHash) {
return
}
this.lastValueHash = contentHash
this.editor.setContent(content)
}
}
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
onUpdate(e) {
const content = e.getHTML()
const contentHash = stringHash(content)
if (contentHash !== this.lastValueHash) {
this.lastValueHash = contentHash
this.$emit('input', content)
}
},
showLinkMenu(attrs) {
this.linkUrl = attrs.href
this.linkMenuIsActive = true
this.$nextTick(() => {
try {
const $el = this.$refs.linkInput.$el.getElementsByTagName('input')[0]
$el.focus()
$el.select()
} catch (err) {}
})
},
hideLinkMenu() {
this.linkUrl = null
this.linkMenuIsActive = false
this.editor.focus()
},
hideMenu(isActive) {
isActive = false
this.hideLinkMenu()
},
setLinkUrl(command, url) {
const links = linkify().match(url)
if (links) {
// add valid link
command({
href: links.pop().url
})
this.hideLinkMenu()
this.editor.focus()
} else if (!url) {
// remove link
command({ href: null })
}
}
}
}
</script>
<style lang="scss">
.ProseMirror {
padding: $space-base;
margin: -$space-base;
min-height: $space-large;
}
.ProseMirror:focus {
outline: none;
}
.editor p.is-empty:first-child::before {
content: attr(data-empty-text);
float: left;
color: $text-color-disabled;
padding-left: $space-xx-small;
pointer-events: none;
height: 0;
}
.menubar__button {
font-weight: normal;
}
li > p {
margin-top: $space-xx-small;
margin-bottom: $space-xx-small;
}
.editor {
&__floating-menu {
position: absolute;
margin-top: -0.25rem;
margin-left: $space-xx-small;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
background-color: #fff;
&.is-active {
opacity: 1;
visibility: visible;
}
}
.menububble {
position: absolute;
// margin-top: -0.5rem;
visibility: hidden;
opacity: 0;
transition: opacity 200ms, visibility 200ms;
// transition-delay: 50ms;
transform: translate(-50%, -10%);
background-color: $background-color-inverse-soft;
// color: $text-color-inverse;
border-radius: $border-radius-base;
padding: $space-xx-small;
box-shadow: $box-shadow-large;
.ds-button {
color: $text-color-inverse;
&.ds-button-hover,
&:hover {
color: $text-color-base;
}
}
&.is-active {
opacity: 1;
visibility: visible;
}
.tooltip-arrow {
left: calc(50% - 10px);
}
input,
button {
border: none;
border-radius: 2px;
}
.ds-input {
height: auto;
}
input {
padding: $space-xx-small $space-x-small;
}
}
}
</style>

View File

@ -0,0 +1,83 @@
import { Extension, Plugin } from 'tiptap'
// import { Slice, Fragment } from 'prosemirror-model'
export default class EventHandler extends Extension {
get name() {
return 'event_handler'
}
get plugins() {
return [
new Plugin({
props: {
transformPastedText(text) {
// console.log('#### transformPastedText', text)
return text.trim()
},
transformPastedHTML(html) {
html = html
// remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
// remove all iframes
.replace(
/(<iframe(?!.*?src=(['"]).*?\2)[^>]*)(>)[^>]*\/*>/gim,
''
)
.replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
// replace all p tags with line breaks (and spaces) only by single linebreaks
// limit linebreaks to max 2 (equivalent to html "br" linebreak)
.replace(/(<br ?\/?>\s*){2,}/gim, '<br>')
// remove additional linebreaks after p tags
.replace(
/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim,
'</p><p>'
)
// remove additional linebreaks inside p tags
.replace(
/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim,
''
)
// remove additional linebreaks when first child inside p tags
.replace(/<p>(\s*<br ?\/?>\s*)+/gim, '<p>')
// remove additional linebreaks when last child inside p tags
.replace(/(\s*<br ?\/?>\s*)+<\/p>/gim, '</p>')
// console.log('#### transformPastedHTML', html)
return html
}
// transformPasted(slice) {
// // console.log('#### transformPasted', slice.content)
// let content = []
// let size = 0
// slice.content.forEach((node, offset, index) => {
// // console.log(node)
// // console.log('isBlock', node.type.isBlock)
// // console.log('childCount', node.content.childCount)
// // console.log('index', index)
// if (node.content.childCount) {
// content.push(node.content)
// size += node.content.size
// }
// })
// console.log(content)
// console.log(slice.content)
// let fragment = Fragment.fromArray(content)
// fragment.size = size
// console.log('#fragment', fragment, slice.content)
// console.log('----')
// console.log('#1', slice)
// // const newSlice = new Slice(fragment, slice.openStart, slice.openEnd)
// slice.fragment = fragment
// // slice.content.content = fragment.content
// // slice.content.size = fragment.size
// console.log('#2', slice)
// // console.log(newSlice)
// console.log('----')
// return slice
// // return newSlice
// }
}
})
]
}
}

View File

@ -55,6 +55,7 @@
context="contribution"
:item-id="post.id"
:name="post.title"
:is-owner="isAuthor"
/>
</no-ssr>
</div>
@ -86,13 +87,16 @@ export default {
computed: {
excerpt() {
// remove all links from excerpt to prevent issues with the serounding link
let excerpt = this.post.contentExcerpt.replace(/<a.*>(.+)<\/a>/gim, '')
let excerpt = this.post.contentExcerpt.replace(/<a.*>(.+)<\/a>/gim, '$1')
// do not display content that is only linebreaks
if (excerpt.replace(/<br>/gim, '').trim() === '') {
excerpt = ''
}
return excerpt
},
isAuthor() {
return this.$store.getters['auth/user'].id === this.post.author.id
}
},
methods: {

View File

@ -0,0 +1,6 @@
{
"SEED_SERVER_HOST": "http://localhost:4001",
"NEO4J_URI": "bolt://localhost:7687",
"NEO4J_USERNAME": "neo4j",
"NEO4J_PASSWORD": "letmein"
}

View File

@ -4,11 +4,7 @@ Feature: Authentication
In order to attribute posts and other contributions to their authors
Background:
Given my account has the following details:
| 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
Given I have a user account
Scenario: Log in
When I visit the "/login" page

View File

@ -14,28 +14,27 @@ Feature: Tags and Categories
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
Given my user account has the role "admin"
And we have a selection of tags and categories as well as posts
And 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 |
| Icon | Name | Posts |
| | Just For Fun | 2 |
| | Happyness & Values | 1 |
| | Health & Wellbeing | 0 |
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 |
Then I can see a list of tags ordered by user count:
| # | Name | Users | Posts |
| 1 | Democracy | 2 | 3 |
| 2 | Ecology | 1 | 1 |
| 3 | Nature | 1 | 2 |

View File

@ -7,21 +7,18 @@ Feature: About me and location
to search for users by location.
Background:
Given I am logged in
Given I have a user account
And 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
And when I refresh the page
Then the name "Hansi" 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
@ -36,10 +33,5 @@ Feature: About me and location
"""
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

@ -9,13 +9,13 @@ Feature: Report and Moderate
Background:
Given we have the following posts in our database:
| Author | Title | Content | Slug |
| David Irving | The Truth about the Holocaust | It never existed! | the-truth-about-the-holocaust |
| Author | id | title | content |
| David Irving | p1 | The Truth about the Holocaust | It never existed! |
Scenario Outline: Report a post from various pages
Given I am logged in with a "user" role
And I see David Irving's post on the <Page>
When I click on "Report Contribution" from the triple dot menu of the post
When I see David Irving's post on the <Page>
And I click on "Report Contribution" from the triple dot menu of the post
And I confirm the reporting dialog because it is a criminal act under German law:
"""
Do you really want to report the contribution "The Truth about the Holocaust"?
@ -45,8 +45,8 @@ Feature: Report and Moderate
Scenario: Review reported content
Given somebody reported the following posts:
| Slug |
| the-truth-about-the-holocaust |
| id |
| p1 |
And I am logged in with a "moderator" role
When I click on the avatar menu in the top right corner
And I click on "Moderation"

View File

@ -0,0 +1,25 @@
Feature: Create a post
As a user
I would like to create a post
To say something to everyone in the community
Background:
Given I have a user account
And I am logged in
And I am on the "landing" page
Scenario: Create a post
When I click on the big plus icon in the bottom right corner to create post
And I choose "My first post" as the title of the post
And I type in the following text:
"""
Human Connection is a free and open-source social network
for active citizenship.
"""
And I click on "Save"
Then I get redirected to "/post/my-first-post/"
And the post was saved successfully
Scenario: See a post on the landing page
Given I previously created a post
Then the post shows up on the landing page at position 1

View File

@ -2,19 +2,6 @@ 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')
@ -23,17 +10,37 @@ When('I navigate to the administration dashboard', () => {
})
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()
table.hashes().forEach(({ Name, Posts }, index) => {
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`).should(
'contain',
Name
)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`).should(
'contain',
Posts
)
})
})
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
Then('I can see a list of tags ordered by user count:', table => {
cy.get('thead')
.find('tr th')
.should('have.length', 4)
lastColumnIsSortedInDescendingOrder()
table.hashes().forEach(({ Name, Users, Posts }, index) => {
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`).should(
'contain',
Name
)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`).should(
'contain',
Users
)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(4)`).should(
'contain',
Posts
)
})
})

View File

@ -3,9 +3,9 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
/* global cy */
let lastReportTitle
let dummyReportedPostTitle = 'Hacker, Freaks und Funktionäre'
let dummyReportedPostSlug = 'hacker-freaks-und-funktionareder-ccc'
let dummyAuthorName = 'Jenny Rostock'
let davidIrvingPostTitle = 'The Truth about the Holocaust'
let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
let davidIrvingName = 'David Irving'
const savePostTitle = $post => {
return $post
@ -23,21 +23,27 @@ Given("I see David Irving's post on the landing page", page => {
})
Given("I see David Irving's post on the post page", page => {
cy.visit(`/post/${dummyReportedPostSlug}`)
cy.contains(dummyReportedPostTitle) // wait
cy.visit(`/post/${davidIrvingPostSlug}`)
cy.contains(davidIrvingPostTitle) // wait
})
Given('I am logged in with a {string} role', role => {
cy.loginAs(role)
cy.factory().create('User', {
email: `${role}@example.org`,
password: '1234',
role
})
cy.login({
email: `${role}@example.org`,
password: '1234'
})
})
When(
'I click on "Report Contribution" from the triple dot menu of the post',
() => {
//TODO: match the created post title, not a dummy post title
cy.contains('.ds-card', dummyReportedPostTitle)
cy.contains('.ds-card', davidIrvingPostTitle)
.find('.content-menu-trigger')
.first()
.click()
cy.get('.popover .ds-menu-item-link')
@ -49,8 +55,7 @@ When(
When(
'I click on "Report User" from the triple dot menu in the user info box',
() => {
//TODO: match the created post author, not a dummy author
cy.contains('.ds-card', dummyAuthorName)
cy.contains('.ds-card', davidIrvingName)
.find('.content-menu-trigger')
.first()
.click()
@ -106,12 +111,8 @@ Then(`I can't see the moderation menu item`, () => {
.should('not.exist')
})
When(/^I confirm the reporting dialog .*:$/, () => {
//TODO: take message from method argument
//TODO: match the right post
const message = 'Do you really want to report the'
When(/^I confirm the reporting dialog .*:$/, message => {
cy.contains(message) // wait for element to become visible
//TODO: cy.get('.ds-modal').contains(dummyReportedPostTitle)
cy.get('.ds-modal').within(() => {
cy.get('button')
.contains('Send Report')
@ -120,22 +121,28 @@ When(/^I confirm the reporting dialog .*:$/, () => {
})
Given('somebody reported the following posts:', table => {
table.hashes().forEach(row => {
//TODO: calll factory here
// const options = Object.assign({}, row, { reported: true })
//create('post', options)
table.hashes().forEach(({ id }) => {
const reporter = {
email: `reporter${id}@example.org`,
password: '1234'
}
cy.factory()
.create('User', reporter)
.authenticateAs(reporter)
.create('Report', {
description: "I don't like this post",
resource: { id, type: 'contribution' }
})
})
})
Then('I see all the reported posts including the one from above', () => {
//TODO: match the right post
cy.get('table tbody').within(() => {
cy.contains('tr', dummyReportedPostTitle)
cy.contains('tr', davidIrvingPostTitle)
})
})
Then('each list item links to the post page', () => {
//TODO: match the right post
cy.contains(dummyReportedPostTitle).click()
cy.contains(davidIrvingPostTitle).click()
cy.location('pathname').should('contain', '/post')
})

View File

@ -4,7 +4,6 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'
let aboutMeText
let myLocation
let myName
const matchNameInUserMenu = name => {
cy.get('.avatar-menu').click() // open
@ -12,18 +11,13 @@ const matchNameInUserMenu = name => {
cy.get('.avatar-menu').click() // close again
}
const setUserName = name => {
When('I save {string} as my new name', name => {
cy.get('input[id=name]')
.clear()
.type(name)
cy.get('[type=submit]')
.click()
.not('[disabled]')
myName = name
}
When('I save {string} as my new name', name => {
setUserName(name)
})
When('I save {string} as my location', location => {
@ -47,31 +41,20 @@ When('I have the following self-description:', text => {
aboutMeText = text
})
When('my username is {string}', name => {
if (myName !== name) {
setUserName(name)
}
matchNameInUserMenu(name)
})
When('people visit my profile page', url => {
cy.visitMyProfile()
cy.openPage('/profile/peter-pan')
})
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)
cy.contains(myLocation)
})
Then('my new username is still there', () => {
matchNameInUserMenu(myName)
Then('the name {string} is still there', name => {
matchNameInUserMenu(name)
})
Then(

View File

@ -1,26 +1,87 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { getLangByName } from '../../support/helpers'
import users from '../../fixtures/users.json'
/* global cy */
let lastPost = {}
const loginCredentials = {
email: 'peterpan@example.org',
password: '1234'
}
const narratorParams = {
name: 'Peter Pan',
...loginCredentials
}
Given('I am logged in', () => {
cy.loginAs('admin')
})
Given('I am logged in as {string}', userType => {
cy.loginAs(userType)
cy.login(loginCredentials)
})
Given('we have a selection of tags and categories as well as posts', () => {
// TODO: use db factories instead of seed data
cy.factory()
.authenticateAs(loginCredentials)
.create('Category', {
id: 'cat1',
name: 'Just For Fun',
slug: 'justforfun',
icon: 'smile'
})
.create('Category', {
id: 'cat2',
name: 'Happyness & Values',
slug: 'happyness-values',
icon: 'heart-o'
})
.create('Category', {
id: 'cat3',
name: 'Health & Wellbeing',
slug: 'health-wellbeing',
icon: 'medkit'
})
.create('Tag', { id: 't1', name: 'Ecology' })
.create('Tag', { id: 't2', name: 'Nature' })
.create('Tag', { id: 't3', name: 'Democracy' })
const someAuthor = {
id: 'authorId',
email: 'author@example.org',
password: '1234'
}
cy.factory()
.create('User', someAuthor)
.authenticateAs(someAuthor)
.create('Post', { id: 'p0' })
.create('Post', { id: 'p1' })
cy.factory()
.authenticateAs(loginCredentials)
.create('Post', { id: 'p2' })
.relate('Post', 'Categories', { from: 'p0', to: 'cat1' })
.relate('Post', 'Categories', { from: 'p1', to: 'cat2' })
.relate('Post', 'Categories', { from: 'p2', to: 'cat1' })
.relate('Post', 'Tags', { from: 'p0', to: 't1' })
.relate('Post', 'Tags', { from: 'p0', to: 't2' })
.relate('Post', 'Tags', { from: 'p0', to: 't3' })
.relate('Post', 'Tags', { from: 'p1', to: 't2' })
.relate('Post', 'Tags', { from: 'p1', to: 't3' })
.relate('Post', 'Tags', { from: 'p2', to: 't3' })
})
Given('my account has the following details:', table => {
// TODO: use db factories instead of seed data
Given('we have the following user accounts:', table => {
table.hashes().forEach(params => {
cy.factory().create('User', params)
})
})
Given('I have a user account', () => {
cy.factory().create('User', narratorParams)
})
Given('my user account has the role {string}', role => {
// TODO: use db factories instead of seed data
cy.factory().create('User', {
role,
...loginCredentials
})
})
When('I log out', cy.logout)
@ -34,10 +95,10 @@ Given('I am on the {string} page', page => {
})
When('I fill in my email and password combination and click submit', () => {
cy.login('admin@example.org', 1234)
cy.login(loginCredentials)
})
When('I refresh the page', () => {
When(/(?:when )?I refresh the page/, () => {
cy.reload()
})
@ -49,7 +110,7 @@ When('I log out through the menu in the top right corner', () => {
})
Then('I can see my name {string} in the dropdown menu', () => {
cy.get('.avatar-menu-popover').should('contain', users.admin.name)
cy.get('.avatar-menu-popover').should('contain', narratorParams.name)
})
Then('I see the login screen again', () => {
@ -63,7 +124,7 @@ Then('I can click on my profile picture in the top right corner', () => {
Then('I am still logged in', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover').contains(users.admin.name)
cy.get('.avatar-menu-popover').contains(narratorParams.name)
})
When('I select {string} in the language menu', name => {
@ -90,9 +151,18 @@ When('I press {string}', label => {
})
Given('we have the following posts in our database:', table => {
table.hashes().forEach(row => {
//TODO: calll factory here
//create('post', row)
table.hashes().forEach(({ Author, id, title, content }) => {
cy.factory()
.create('User', {
name: Author,
email: `${Author}@example.org`,
password: '1234'
})
.authenticateAs({
email: `${Author}@example.org`,
password: '1234'
})
.create('Post', { id, title, content })
})
})
@ -103,3 +173,42 @@ Then('I see a success message:', message => {
When('I click on the avatar menu in the top right corner', () => {
cy.get('.avatar-menu').click()
})
When(
'I click on the big plus icon in the bottom right corner to create post',
() => {
cy.get('.post-add-button').click()
}
)
Given('I previously created a post', () => {
cy.factory()
.authenticateAs(loginCredentials)
.create('Post', lastPost)
})
When('I choose {string} as the title of the post', title => {
lastPost.title = title.replace('\n', ' ')
cy.get('input[name="title"]').type(lastPost.title)
})
When('I type in the following text:', text => {
lastPost.content = text.replace('\n', ' ')
cy.get('.ProseMirror').type(lastPost.content)
})
Then('the post shows up on the landing page at position {int}', index => {
cy.openPage('landing')
const selector = `:nth-child(${index}) > .ds-card > .ds-card-content`
cy.get(selector).should('contain', lastPost.title)
cy.get(selector).should('contain', lastPost.content)
})
Then('I get redirected to {string}', route => {
cy.location('pathname').should('contain', route)
})
Then('the post was saved successfully', () => {
cy.get('.ds-card-header > .ds-heading').should('contain', lastPost.title)
cy.get('.content').should('contain', lastPost.content)
})

View File

@ -35,14 +35,7 @@ Cypress.Commands.add('switchLanguage', (name, force) => {
}
})
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) => {
Cypress.Commands.add('login', ({ email, password }) => {
cy.visit(`/login`)
cy.get('input[name=email]')
.trigger('focus')
@ -56,11 +49,6 @@ Cypress.Commands.add('login', (email, password) => {
cy.location('pathname').should('eq', '/') // we're in!
})
Cypress.Commands.add('loginAs', role => {
role = role || 'admin'
cy.login(users[role].email, users[role].password)
})
Cypress.Commands.add('logout', (email, password) => {
cy.visit(`/logout`)
cy.location('pathname').should('contain', '/login') // we're out

View File

@ -0,0 +1,43 @@
// TODO: find a better way how to import the factories
import Factory from '../../../Nitro-Backend/src/seed/factories'
import { getDriver } from '../../../Nitro-Backend/src/bootstrap/neo4j'
const neo4jDriver = getDriver({
uri: Cypress.env('NEO4J_URI'),
username: Cypress.env('NEO4J_USERNAME'),
password: Cypress.env('NEO4J_PASSWORD')
})
const factory = Factory({ neo4jDriver })
const seedServerHost = Cypress.env('SEED_SERVER_HOST')
beforeEach(async () => {
await factory.cleanDatabase({ seedServerHost, neo4jDriver })
})
Cypress.Commands.add('factory', () => {
return Factory({ seedServerHost })
})
Cypress.Commands.add(
'create',
{ prevSubject: true },
(factory, node, properties) => {
return factory.create(node, properties)
}
)
Cypress.Commands.add(
'relate',
{ prevSubject: true },
(factory, node, relationship, properties) => {
return factory.relate(node, relationship, properties)
}
)
Cypress.Commands.add(
'authenticateAs',
{ prevSubject: true },
(factory, loginCredentials) => {
return factory.authenticateAs(loginCredentials)
}
)

View File

@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax:
import './commands'
import './factories'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -8,9 +8,9 @@ services:
volumes:
- .:/nitro-web
- node_modules:/nitro-web/node_modules
- node_modules_styleguide:/nitro-web/styleguide/node_modules
- nuxt:/nitro-web/.nuxt
command: yarn run dev
volumes:
node_modules:
node_modules_styleguide:
nuxt:

View File

@ -5,3 +5,5 @@ services:
build:
context: .
target: build-and-test
environment:
- BACKEND_URL=http://backend:4123

View File

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

28
graphql/PostMutations.js Normal file
View File

@ -0,0 +1,28 @@
import gql from 'graphql-tag'
export default app => {
return {
CreatePost: gql(`
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
id
title
slug
content
contentExcerpt
}
}
`),
UpdatePost: gql(`
mutation($id: ID!, $title: String!, $content: String!) {
UpdatePost(id: $id, title: $title, content: $content) {
id
title
slug
content
contentExcerpt
}
}
`)
}
}

View File

@ -5,7 +5,8 @@
"create": "Erstellen",
"save": "Speichern",
"edit": "Bearbeiten",
"delete": "Löschen"
"delete": "Löschen",
"cancel": "Abbrechen"
},
"login": {
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
@ -100,6 +101,14 @@
"reporter": "gemeldet von"
}
},
"contribution": {
"edit": "Beitrag bearbeiten",
"delete": "Beitrag löschen"
},
"comment": {
"edit": "Kommentar bearbeiten",
"delete": "Kommentar löschen"
},
"disable": {
"user": {
"title": "Nutzer sperren",

View File

@ -5,7 +5,8 @@
"create": "Create",
"save": "Save",
"edit": "Edit",
"delete": "Delete"
"delete": "Delete",
"cancel": "Cancel"
},
"login": {
"copy": "If you already have a human-connection account, login here.",
@ -100,6 +101,14 @@
"reporter": "reported by"
}
},
"contribution": {
"edit": "Edit Contribution",
"delete": "Delete Contribution"
},
"comment": {
"edit": "Edit Comment",
"delete": "Delete Comment"
},
"disable": {
"user": {
"title": "Disable User",

View File

@ -43,7 +43,14 @@ module.exports = {
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: pkg.description }
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
script: [
{
src:
'https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver',
body: true
}
]
},
/*
@ -63,10 +70,10 @@ module.exports = {
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~/plugins/styleguide.js', ssr: true },
{ 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 },
{ src: '~/plugins/v-tooltip.js', ssr: false },
{ src: '~/plugins/izi-toast.js', ssr: false },
@ -96,7 +103,7 @@ module.exports = {
],
styleResources: {
scss: ['~/styleguide/src/system/styles/shared.scss']
scss: ['@human-connection/styleguide/dist/shared.scss']
},
/*
@ -173,14 +180,6 @@ module.exports = {
exclude: /(node_modules)/
})
}
config.resolve.alias['@@'] = path.resolve(
__dirname,
'./styleguide/src/system'
)
config.module.rules.push({
resourceQuery: /blockType=docs/,
loader: require.resolve('./styleguide/src/loader/docs-trim-loader.js')
})
const svgRule = config.module.rules.find(rule => rule.test.test('.svg'))
svgRule.test = /\.(png|jpe?g|gif|webp)$/
config.module.rules.push({

View File

@ -10,8 +10,8 @@
"start": "cross-env node server/index.js",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue .",
"styleguide": "cd ./styleguide && yarn dev",
"styleguide:build": "cd ./styleguide && yarn build:lib && cd ../",
"styleguide": "echo 'Command styleguide is Deprecated!'",
"styleguide:build": "echo 'Command styleguide:build is Deprecated!'",
"test": "jest",
"precommit": "yarn lint",
"e2e:local": "cypress run --headed",
@ -21,6 +21,7 @@
"nonGlobalStepDefinitions": true
},
"jest": {
"verbose": true,
"moduleFileExtensions": [
"js",
"json",
@ -32,12 +33,8 @@
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"^@@/(.*)$": "<rootDir>/styleguide/src/system/$1",
"^~/(.*)$": "<rootDir>/$1"
},
"modulePathIgnorePatterns": [
"<rootDir>/styleguide"
]
}
},
"dependencies": {
"@nuxtjs/apollo": "4.0.0-rc4",
@ -45,26 +42,31 @@
"@nuxtjs/dotenv": "~1.3.0",
"@nuxtjs/style-resources": "~0.1.2",
"accounting": "~0.4.1",
"apollo-cache-inmemory": "~1.4.3",
"apollo-client": "~2.4.13",
"apollo-cache-inmemory": "~1.5.1",
"apollo-client": "~2.5.1",
"cookie-universal-nuxt": "~2.0.14",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27",
"express": "~4.16.4",
"graphql": "~14.1.1",
"jsonwebtoken": "~8.5.0",
"nuxt": "~2.4.3",
"linkify-it": "~2.1.0",
"nuxt": "~2.4.5",
"nuxt-env": "~0.1.0",
"portal-vue": "~1.5.1",
"@human-connection/styleguide": "~0.5.0",
"v-tooltip": "~2.0.0-rc.33",
"vue-count-to": "~1.0.13",
"string-hash": "^1.1.3",
"tiptap": "^1.13.0",
"tiptap-extensions": "^1.13.0",
"vue-izitoast": "1.1.2",
"vue-sweetalert-icons": "~3.2.0",
"vuex-i18n": "~1.11.0"
},
"devDependencies": {
"@babel/core": "~7.3.3",
"@babel/preset-env": "~7.3.1",
"@babel/core": "~7.3.4",
"@babel/preset-env": "~7.3.4",
"@vue/cli-shared-utils": "~3.4.0",
"@vue/eslint-config-prettier": "~4.0.1",
"@vue/server-test-utils": "~1.0.0-beta.29",

View File

@ -13,6 +13,16 @@
<hc-post-card :post="post" />
</ds-flex-item>
</ds-flex>
<no-ssr>
<ds-button
v-tooltip="{content: 'Create a new Post', placement: 'left', delay: { show: 500 }}"
:path="{ name: 'post-create' }"
class="post-add-button"
icon="plus"
size="x-large"
primary
/>
</no-ssr>
<hc-load-more
v-if="true"
:loading="$apollo.loading"
@ -82,7 +92,7 @@ export default {
query() {
return gql(`
query Post($first: Int, $offset: Int) {
Post(first: $first, offset: $offset) {
Post(first: $first, offset: $offset, orderBy: createdAt_desc) {
id
title
contentExcerpt
@ -123,8 +133,20 @@ export default {
first: this.pageSize,
offset: 0
}
}
},
fetchPolicy: 'cache-and-network'
}
}
}
</script>
<style lang="scss">
.post-add-button {
z-index: 100;
position: fixed;
top: 100vh;
left: 100vw;
transform: translate(-120%, -120%);
box-shadow: $box-shadow-x-large;
}
</style>

View File

@ -113,16 +113,16 @@ export default {
}
}
},
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
redirect('/')
}
},
computed: {
pending() {
return this.$store.getters['auth/pending']
}
},
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
redirect('/')
}
},
mounted() {
setTimeout(() => {
// NOTE: quick fix for jumping flexbox implementation

View File

@ -12,6 +12,7 @@
context="contribution"
:item-id="post.id"
:name="post.title"
:is-owner="isAuthor(post.author.id)"
/>
</no-ssr>
<ds-space margin-bottom="small" />
@ -97,6 +98,7 @@
style="float-right"
:item-id="comment.id"
:name="comment.author.name"
:is-owner="isAuthor(comment.author.id)"
/>
</no-ssr>
<!-- eslint-disable vue/no-v-html -->
@ -160,6 +162,11 @@ export default {
this.title = this.post.title
}
},
methods: {
isAuthor(id) {
return this.$store.getters['auth/user'].id === id
}
},
apollo: {
Post: {
query() {

View File

@ -38,7 +38,7 @@
<ds-flex-item
v-for="relatedPost in post.relatedContributions"
:key="relatedPost.id"
:width="{ base: '50%' }"
:width="{ base: '100%', lg: 1 }"
>
<hc-post-card :post="relatedPost" />
</ds-flex-item>
@ -70,7 +70,7 @@ export default {
},
computed: {
post() {
return this.Post ? this.Post[0] : {}
return this.Post ? this.Post[0] || {} : {}
}
},
apollo: {

24
pages/post/create.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<ds-flex
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
&nbsp;
</ds-flex-item>
</ds-flex>
</template>
<script>
import gql from 'graphql-tag'
import HcContributionForm from '~/components/ContributionForm.vue'
export default {
components: {
HcContributionForm
}
}
</script>

77
pages/post/edit/_id.vue Normal file
View File

@ -0,0 +1,77 @@
<template>
<ds-flex
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form :contribution="contribution" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
&nbsp;
</ds-flex-item>
</ds-flex>
</template>
<script>
import gql from 'graphql-tag'
import HcContributionForm from '~/components/ContributionForm.vue'
export default {
components: {
HcContributionForm
},
computed: {
user() {
return this.$store.getters['auth/user']
},
author() {
return this.contribution ? this.contribution.author : {}
},
contribution() {
return this.Post ? this.Post[0] : {}
}
},
watch: {
contribution() {
if (this.author.id !== this.user.id) {
throw new Error(`You can't edit that!`)
}
}
},
apollo: {
Post: {
query() {
return gql(`
query($id: ID!) {
Post(id: $id) {
id
title
content
createdAt
slug
image
author {
id
}
tags {
name
}
categories {
id
name
icon
}
}
}
`)
},
variables() {
return {
id: this.$route.params.id || 'p1'
}
},
fetchPolicy: 'cache-and-network'
}
}
}
</script>

View File

@ -23,6 +23,7 @@
context="user"
:item-id="user.id"
:name="user.name"
:is-owner="myProfile"
/>
</no-ssr>
<ds-space margin="small">
@ -251,6 +252,17 @@
</ds-flex>
</ds-card>
</ds-flex-item>
<ds-flex-item style="text-align: center">
<ds-button
v-if="myProfile"
v-tooltip="{content: 'Create a new Post', placement: 'left', delay: { show: 500 }}"
:path="{ name: 'post-create' }"
class="profile-post-add-button"
icon="plus"
size="large"
primary
/>
</ds-flex-item>
<template v-if="activePosts.length">
<ds-flex-item
v-for="post in activePosts"

View File

@ -48,101 +48,90 @@
<script>
import gql from 'graphql-tag'
import { mapGetters } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
import { CancelToken } from 'axios'
import find from 'lodash/find'
let timeout
const mapboxToken = process.env.MAPBOX_TOKEN
const query = gql`
query getUser($id: ID) {
User(id: $id) {
id
name
locationName
about
}
}
`
const mutation = gql`
mutation($id: ID!, $name: String, $locationName: String, $about: String) {
UpdateUser(
id: $id
name: $name
locationName: $locationName
about: $about
) {
id
name
locationName
about
}
}
`
export default {
data() {
return {
axiosSource: null,
cities: [],
sending: false,
form: {
name: null,
locationName: null,
about: null
}
formData: {}
}
},
computed: {
...mapGetters({
user: 'auth/user'
})
},
watch: {
user: {
immediate: true,
handler: function(user) {
this.form = {
name: user.name,
locationName: user.locationName,
about: user.about
}
currentUser: 'auth/user'
}),
form: {
get: function() {
const { name, locationName, about } = this.currentUser
return { name, locationName, about }
},
set: function(formData) {
this.formData = formData
}
}
},
methods: {
...mapMutations({
setCurrentUser: 'auth/SET_USER'
}),
submit() {
this.sending = true
const { name, about } = this.formData
let { locationName } = this.formData
locationName = locationName && (locationName['label'] || locationName)
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
mutation,
variables: {
id: this.user.id,
name: this.form.name,
locationName: this.form.locationName
? this.form.locationName['label'] || this.form.locationName
: null,
about: this.form.about
id: this.currentUser.id,
name,
locationName,
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 })
const { name, locationName, about } = UpdateUser
this.setCurrentUser({
...this.currentUser,
name,
locationName,
about
})
}
// 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 => {
this.$toast.success('Updated user')

View File

@ -1,5 +0,0 @@
import Vue from 'vue'
import DesignSystem from '@@'
import '@@/styles/main.scss'
Vue.use(DesignSystem)

6
plugins/styleguide.js Normal file
View File

@ -0,0 +1,6 @@
import Vue from 'vue'
import Styleguide from '@human-connection/styleguide'
import '@human-connection/styleguide/dist/system.css'
// import '@human-connection/styleguide/dist/shared.scss'
Vue.use(Styleguide)

View File

@ -57,90 +57,70 @@ export const actions = {
if (!token) {
return
}
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()) {
await dispatch('logout')
}
return getters.isLoggedIn
},
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
locationName
about
}
async fetchCurrentUser({ commit, dispatch }) {
const client = this.app.apolloProvider.defaultClient
const {
data: { currentUser }
} = await client.query({
query: gql(`{
currentUser {
id
name
slug
email
avatar
role
about
locationName
}
`),
variables: { id: getters.user.id }
})
.then(({ data }) => {
const user = data.User.pop()
if (user.id && user.email) {
commit('SET_USER', user)
}
})
return getters.user
}`)
})
if (!currentUser) return dispatch('logout')
commit('SET_USER', currentUser)
return currentUser
},
async login({ commit }, { email, password }) {
async login({ commit, dispatch }, { email, password }) {
commit('SET_PENDING', true)
try {
const res = await this.app.apolloProvider.defaultClient
.mutate({
mutation: gql(`
const client = this.app.apolloProvider.defaultClient
const {
data: { login }
} = await client.mutate({
mutation: gql(`
mutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
id
name
slug
email
avatar
role
locationName
about
token
}
login(email: $email, password: $password)
}
`),
variables: { email, password }
})
.then(({ data }) => data && data.login)
await this.app.$apolloHelpers.onLogin(res.token)
commit('SET_TOKEN', res.token)
const userData = Object.assign({}, res)
delete userData.token
commit('SET_USER', userData)
variables: { email, password }
})
await this.app.$apolloHelpers.onLogin(login)
commit('SET_TOKEN', login)
await dispatch('fetchCurrentUser')
} catch (err) {
throw new Error(err)
} finally {
commit('SET_PENDING', false)
}
},
async logout({ commit }) {
commit('SET_USER', null)
commit('SET_TOKEN', null)
return this.app.$apolloHelpers.onLogout()
},
register(
{ dispatch, commit },
{ email, password, inviteCode, invitedByUserId }

View File

@ -2,23 +2,21 @@ import { getters, mutations, actions } from './auth.js'
let state
let commit
let dispatch
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 currentUser = {
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'
}
const successfulLoginResponse = { data: { login: token } }
const successfulCurrentUserResponse = { data: { currentUser } }
const incorrectPasswordResponse = {
data: {
login: null
@ -39,6 +37,7 @@ const incorrectPasswordResponse = {
beforeEach(() => {
commit = jest.fn()
dispatch = jest.fn(() => Promise.resolve())
})
describe('getters', () => {
@ -55,34 +54,67 @@ describe('getters', () => {
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 }
describe('init', () => {
const theAction = () => {
const module = {
app: {
$apolloHelpers: {
getToken: () => token
}
}
action = actions.login.bind(module)
await action(
{ commit },
{ email: 'user@example.org', password: '1234' }
)
}
action = actions.init.bind(module)
return action({ commit, dispatch })
}
describe('client-side', () => {
beforeEach(() => {
process.server = false
})
afterEach(() => {
action = null
it('returns', async () => {
await theAction()
expect(dispatch.mock.calls).toEqual([])
expect(commit.mock.calls).toEqual([])
})
})
describe('server-side', () => {
beforeEach(() => {
process.server = true
})
it('saves the JWT Bearer token', () => {
it('fetches the current user', async () => {
await theAction()
expect(dispatch.mock.calls).toEqual([['fetchCurrentUser']])
})
it('saves the JWT Bearer token', async () => {
await theAction()
expect(commit.mock.calls).toEqual(
expect.arrayContaining([['SET_TOKEN', token]])
)
})
})
})
describe('fetchCurrentUser', () => {
describe('given a successful response', () => {
beforeEach(async () => {
const module = {
app: {
apolloProvider: {
defaultClient: {
query: jest.fn(() =>
Promise.resolve(successfulCurrentUserResponse)
)
}
}
}
}
action = actions.fetchCurrentUser.bind(module)
await action({ commit })
})
it('saves user data without token', () => {
expect(commit.mock.calls).toEqual(
@ -102,6 +134,44 @@ describe('actions', () => {
])
)
})
})
})
describe('login', () => {
describe('given valid credentials and a successful response', () => {
beforeEach(async () => {
const module = {
app: {
apolloProvider: {
defaultClient: {
mutate: jest.fn(() => Promise.resolve(successfulLoginResponse))
}
},
$apolloHelpers: {
onLogin: jest.fn(() => Promise.resolve())
}
}
}
action = actions.login.bind(module)
await action(
{ commit, dispatch },
{ 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('fetches the user', () => {
expect(dispatch.mock.calls).toEqual([['fetchCurrentUser']])
})
it('saves pending flags in order', () => {
expect(commit.mock.calls).toEqual(

View File

@ -1,3 +0,0 @@
> 1%
last 2 versions
not ie <= 8

View File

@ -1,14 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: ['plugin:vue/strongly-recommended', '@vue/prettier'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}

23
styleguide/.gitignore vendored
View File

@ -1,23 +0,0 @@
.DS_Store
node_modules
# local env files
.env.local
.env.*.local
/src/system/tokens/generated
/src/system/icons/generated
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

View File

@ -1,6 +0,0 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"bracketSpacing": true
}

View File

@ -1,57 +0,0 @@
# CION Vue Design System
CION is a Design System build primary for Vue applications. You can use it as a starting point for building your own Design System.
The system utilizes design tokens, a living styleguide with integrated code playgrounds and reusable components for common UI tasks.
Living styleguide demo: https://styleguide.cion.visualjerk.de
Landing page demo: https://cion.visualjerk.de
Integrate it in your application: [Quick Start](https://github.com/visualjerk/vue-cion-design-system/wiki/Quick-Start)
[![Screenshot](./preview/customize.png)](https://github.com/visualjerk/vue-cion-design-system/raw/master/preview/customize.png)
## Project setup
```
yarn install
```
## Developing
Compiles and hot-reloads living styleguide
```
yarn dev
```
## Building
### Living styleguide
Compiles living styleguide to `./docs`
```
yarn build
```
### Library
Compiles design system as a library to `./dist`
```
yarn build:lib
```
## Helper
### Serve living styleguide locally
```
yarn serve
```
### Lints and fixes files
```
yarn lint
```

View File

@ -1,4 +0,0 @@
module.exports = {
presets: ['@vue/app'],
plugins: ['@babel/plugin-syntax-dynamic-import']
}

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set segmentCount to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
// Otherwise, leave segmentCount as 0.
var segmentCount = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

View File

@ -1 +0,0 @@
styleguide.cion.visualjerk.de

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1 +0,0 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><script src=/babel-standalone.js></script><title>CION - Vue Design System</title><link href=/css/app.e38d5069.css rel=preload as=style><link href=/css/chunk-vendors.7a428b56.css rel=preload as=style><link href=/js/app.f428b946.js rel=preload as=script><link href=/js/chunk-vendors.0a7f54fd.js rel=preload as=script><link href=/css/chunk-vendors.7a428b56.css rel=stylesheet><link href=/css/app.e38d5069.css rel=stylesheet></head><body><noscript><strong>We're sorry but this CION doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.0a7f54fd.js></script><script src=/js/app.f428b946.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,24 +0,0 @@
module.exports = {
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@@/(.*)$': '<rootDir>/src/system/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)|**/spec.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/'
}

View File

@ -1,76 +0,0 @@
{
"name": "vue-cion-design-system",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "http-server ./docs -o -s",
"build": "yarn theo && vue-cli-service build",
"lint": "vue-cli-service lint --no-fix",
"dev": "npm-run-all --parallel theo:onchange theo servedev",
"servedev": "vue-cli-service serve --open",
"build:lib": "yarn theo && cross-env BUILD=library vue-cli-service build --target lib --name system ./src/library.js",
"theo": "theo ./src/system/tokens/tokens.yml --transform web --format map.scss,scss,raw.json,json --dest ./src/system/tokens/generated",
"theo:onchange": "onchange \"./src/system/tokens/*.yml\" -- npm run theo",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"portal-vue": "~1.5.1",
"vue": "~2.6.6"
},
"devDependencies": {
"@babel/core": "~7.2.2",
"@babel/plugin-syntax-dynamic-import": "~7.2.0",
"@babel/standalone": "~7.3.2",
"@vue/cli-plugin-babel": "~3.4.0",
"@vue/cli-plugin-eslint": "~3.4.0",
"@vue/cli-plugin-unit-jest": "~3.4.0",
"@vue/cli-service": "~3.4.0",
"@vue/test-utils": "~1.0.0-beta.29",
"async-validator": "~1.10.1",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "~24.1.0",
"babel-plugin-transform-require-context": "~0.1.1",
"cheerio": "~1.0.0-rc.2",
"clipboard-copy": "~2.0.1",
"clone-deep": "~4.0.1",
"codemirror": "~5.43.0",
"cross-env": "~5.2.0",
"dot-prop": "~4.2.0",
"lodash": "~4.17.11",
"markdown-it": "~8.4.2",
"markdown-it-abbr": "~1.0.4",
"markdown-it-deflist": "~2.0.3",
"markdown-it-emoji": "~1.4.0",
"markdown-it-footnote": "~3.0.1",
"markdown-it-ins": "~2.0.0",
"markdown-it-katex": "~2.0.3",
"markdown-it-mark": "~2.0.0",
"markdown-it-sub": "~1.0.0",
"markdown-it-sup": "~1.0.0",
"markdown-it-task-lists": "~2.1.1",
"node-sass": "~4.11.0",
"npm-run-all": "~4.1.5",
"onchange": "~5.2.0",
"raw-loader": "~1.0.0",
"sass-loader": "~7.1.0",
"theo": "~8.1.1",
"vue-click-outside": "~1.0.7",
"vue-docgen-api": "~2.6.12",
"vue-router": "~3.0.2",
"vue-svg-loader": "~0.12.0",
"vue-template-compiler": "~2.6.6",
"vuep": "git+https://github.com/visualjerk/vuep.git#fix-iframe-firefox",
"webpack-bundle-analyzer": "~3.0.4",
"webpack-merge-and-include-globally": "~2.1.14"
},
"author": "visualjerk",
"main": "./dist/system.umd.min.js",
"files": [
"dist/*",
"src/*",
"public/*",
"*.json",
"*.js"
],
"license": "MIT"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set segmentCount to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
// Otherwise, leave segmentCount as 0.
var segmentCount = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

View File

@ -1 +0,0 @@
styleguide.cion.visualjerk.de

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="/babel-standalone.js"></script>
<title>Human Connection - Styleguide</title>
</head>
<body>
<noscript>
<strong>We're sorry but this CION doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,8 +0,0 @@
import system from './system'
import { tokens } from './system/tokens'
import * as utils from './system/utils'
import * as mixins from './system/mixins'
export { tokens, utils, mixins }
export default system

View File

@ -1,9 +0,0 @@
module.exports = function(source, map) {
this.callback(
null,
`export default function (Component) {
Component.options.__docs = ${JSON.stringify(source)}
}`,
map
)
}

View File

@ -1,7 +0,0 @@
module.exports = function(source, map) {
this.callback(
null,
`export default function () {}`,
map
)
}

View File

@ -1,13 +0,0 @@
// Get jsdocs meta from component with
// @url: https://github.com/vue-styleguidist/vue-docgen-api
const parseSource = require('vue-docgen-api').parseSource
module.exports = function(source) {
const callback = this.async()
const content = JSON.stringify(source)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')
.replace(/\\n/g, '\n')
const component = parseSource(content, this.resourcePath)
callback(null, `module.exports = ${JSON.stringify(component)}`)
}

View File

@ -1 +0,0 @@
require('./styleguide')

View File

@ -1,32 +0,0 @@
<template>
<div id="app">
<ds-page ref="page">
<template slot="brand">
<router-link to="/">
<ds-logo/>
</router-link>
</template>
<navigation
@navigate="$refs.page.closeDrawer()"
slot="sidebar"/>
<navigation
@navigate="$refs.page.closeDrawer()"
slot="drawer"/>
<router-view/>
</ds-page>
</div>
</template>
<script>
import Navigation from './components/Navigation'
export default {
name: 'App',
components: {
Navigation
}
}
</script>
<style lang="scss">
</style>

View File

@ -1,168 +0,0 @@
<template>
<div :class="`${iframe ? 'vuep-iframe' : ''}`">
<vuep
:value="template"
:options="{ theme: 'vueds' }"
:iframe="iframe" />
</div>
</template>
<script>
import Vuep from 'vuep'
import 'vuep/dist/vuep.css'
export default {
name: 'CodeExample',
data() {
return {
iframe: false
}
},
props: {
code: {
type: String,
required: true
}
},
components: {
Vuep
},
computed: {
template() {
return this.getCode()
}
},
methods: {
getCode() {
const codeLines = this.code.split('\n')
const codeTypeMatch = codeLines[0].trim().match(/^[A-Za-z]+$/g)
if (codeTypeMatch) {
codeLines.shift()
const codeType = codeTypeMatch[0]
if (codeType === 'iframe') {
this.iframe = true
}
}
while (codeLines[0].trim() === '') {
codeLines.shift()
}
while (codeLines[codeLines.length - 1].trim() === '') {
codeLines.pop()
}
if (codeLines[0].trim() === '<template>') {
return codeLines.join('\n')
}
const code = codeLines.map(line => ' ' + line).join('\n')
/* eslint-disable */
return `<template>
<div>
${code}
</div>
</template>
<script><\/script>`
}
}
}
</script>
<style lang="scss">
.vuep {
display: flex;
height: auto;
font-family: inherit;
flex-direction: column-reverse;
}
.vuep-editor {
width: auto;
height: auto;
margin-right: 0;
}
.vuep-preview {
width: auto;
height: auto;
border-radius: 0;
border: $border-size-base solid $border-color-softer;
padding: $space-base;
margin-bottom: $space-small;
overflow: visible;
.vuep-iframe & {
padding: 0;
min-height: 600px;
}
}
// Codemirror Theme
$codemirror-background: $background-color-soft;
$codemirror-primary: $color-primary;
.cm-s-vueds {
font-size: 1em;
line-height: 1.5em;
font-family: $font-family-code;
letter-spacing: 0.3px;
word-spacing: 1px;
background: $codemirror-background;
color: $text-color-soft;
border: $border-size-base solid $border-color-softer;
.CodeMirror-lines {
padding: 8px 0;
}
.CodeMirror-gutters {
background-color: $codemirror-background;
border: none;
border-right: $border-size-base solid $border-color-softer;
padding-right: $space-x-small;
z-index: 3;
}
div.CodeMirror-cursor {
border-left: 2px solid $text-color-base;
}
.CodeMirror-activeline-background {
background: rgba($codemirror-primary, 0.1);
}
.CodeMirror-selected {
background: rgba($codemirror-primary, 0.1);
}
.cm-comment {
font-style: italic;
color: $text-color-softer;
}
.cm-tag {
color: $codemirror-primary;
}
.cm-attribute {
color: $text-color-warning;
}
.cm-keyword {
color: $text-color-danger;
}
.cm-string {
color: $text-color-success;
}
.cm-property {
color: $text-color-warning;
}
.cm-variable-2 {
color: $text-color-danger;
}
.cm-atom {
color: $text-color-success;
}
.cm-number {
color: $text-color-danger;
}
.cm-operator {
color: $codemirror-primary;
}
.CodeMirror-linenumber {
color: $text-color-softer;
}
}
</style>

View File

@ -1,142 +0,0 @@
<template>
<div>
<ds-page-title :heading="component.name | componentName" />
<ds-container>
<ds-space
v-if="component.tags"
margin-top="base">
<template
v-for="(tagGroup, name) in component.tags">
<ds-tag
v-for="(tag, index) in tagGroup"
:color="tagColor(tag)"
:key="`${name}${index}`">
{{ tagDescription(tag) }}
</ds-tag>&nbsp;
</template>
</ds-space>
<ds-space margin-bottom="xx-large">
<ds-text size="x-large">{{ component.description }}</ds-text>
</ds-space>
<ds-space
v-for="(part, index) in docParts"
margin-bottom="xx-large"
:key="index">
<ds-space>
<markdown :content="part.description"/>
</ds-space>
<code-example
:code="part.example"
v-if="part.example"/>
</ds-space>
<ds-space margin-bottom="xx-large">
<component-options-doc :component="component" />
</ds-space>
<ds-space
margin-bottom="xx-large"
v-if="component.children"
v-for="child in component.children"
:key="child.name">
<component-options-doc :component="child" />
</ds-space>
</ds-container></div>
</template>
<script>
import CodeExample from './CodeExample'
import ComponentOptionsDoc from './ComponentOptionsDoc'
export default {
name: 'ComponentDoc',
props: {
component: {
type: Object,
required: true
}
},
components: {
CodeExample,
ComponentOptionsDoc
},
data() {
return {
propFields: {
name: {
label: 'Prop Name',
width: '20%'
},
type: {
label: 'Type',
width: '20%'
},
default: {
label: 'Default',
width: '20%'
},
description: 'Description'
}
}
},
computed: {
componentProps() {
return Object.keys(this.component.props).map(name => {
return {
name,
...this.component.props[name]
}
})
},
componentSlots() {
return Object.keys(this.component.slots).map(name => {
return {
name,
...this.component.slots[name]
}
})
},
docParts() {
const component = this.component.component
if (!component.__docs) {
return []
}
const parts = component.__docs.split('```')
let i = 0
const parsed = parts.reduce((result, part, index) => {
if (index % 2 === 0) {
result[i] = {
description: part
}
} else {
result[i].example = part
i++
}
return result
}, [])
return parsed
}
},
methods: {
tagColor(tag) {
if (tag.title === 'deprecated') {
return 'warning'
}
if (tag.title === 'see') {
return 'primary'
}
return 'inverse'
},
tagDescription(tag) {
if (tag.description === true) {
return tag.title
}
if (tag.title === 'see') {
return `Child of ${tag.description}`
}
return `${tag.title} ${tag.description}`
}
}
}
</script>
<style lang="scss">
</style>

View File

@ -1,32 +0,0 @@
<template>
<ds-flex-item>
<ds-card
:header="name">
{{ component.description }}
<template slot="footer">
<ds-button
:path="{ name: component.name }"
primary>
{{ name }} Details
</ds-button>
</template>
</ds-card>
</ds-flex-item>
</template>
<script>
export default {
name: 'ComponentItem',
props: {
component: {
type: Object,
required: true
}
},
computed: {
name() {
return this.$options.filters.componentName(this.component.name)
}
}
}
</script>

View File

@ -1,215 +0,0 @@
<template>
<div>
<ds-space v-if="componentProps">
<ds-heading tag="h2">{{ component.name | componentName }} Props</ds-heading>
<ds-card>
<ds-table
:data="componentProps"
:fields="propFields">
<template
slot="name"
slot-scope="{row}">
<ds-code inline>
{{ row.name | kebabCase }}
</ds-code>
<div v-if="row.required">
<ds-tag v-if="row.required" color="warning">required</ds-tag>
</div>
<ds-space :margin-bottom="null" margin-top="small">
<div v-if="row.options">
<ds-chip size="small" v-for="option in row.options" :key="option">
{{ option }}
</ds-chip>
</div>
<ds-text color="soft">{{ row.description }}</ds-text>
</ds-space>
</template>
<template
slot="type"
slot-scope="{row}">
<ds-chip
v-for="type in row.types"
:key="type"
inline>
{{ type }}
</ds-chip>
</template>
<template
slot="default"
slot-scope="{row}">
<ds-chip
v-if="row.defaultValue"
color="primary">
<template v-if="row.default">
{{ row.default }}
</template>
<template v-else-if="row.defaultValue.func">
Function()
</template>
<template v-else>
{{ row.defaultValue.value }}
</template>
</ds-chip>
</template>
</ds-table>
</ds-card>
</ds-space>
<ds-space v-if="componentSlots && componentSlots.length">
<ds-heading tag="h2">{{ component.name | componentName }} Slots</ds-heading>
<ds-card>
<ds-table
:data="componentSlots"
:fields="slotFields">
<ds-code
slot="name"
slot-scope="{row}"
inline>
{{ row.name }}
</ds-code>
<ds-text
color="soft"
slot="description"
slot-scope="{row}">
{{ row.description }}
</ds-text>
</ds-table>
</ds-card>
</ds-space>
<ds-space v-if="componentEvents && componentEvents.length">
<ds-heading tag="h2">{{ component.name | componentName }} Events</ds-heading>
<ds-card>
<ds-table
:data="componentEvents"
:fields="eventFields">
<ds-code
slot="name"
slot-scope="{row}"
inline>
@{{ row.name }}
</ds-code>
<ds-text
color="soft"
slot="description"
slot-scope="{row}">
{{ row.description }}
</ds-text>
</ds-table>
</ds-card>
</ds-space>
</div>
</template>
<script>
export default {
name: 'ComponentOptionsDoc',
props: {
component: {
type: Object,
required: true
}
},
data() {
return {
propFields: {
name: {
label: 'Name'
},
type: {
label: 'Type',
width: '20%'
},
default: {
label: 'Default',
width: '20%'
}
},
slotFields: {
name: {
label: 'Name',
width: '20%'
},
description: 'Description'
},
eventFields: {
name: {
label: 'Name',
width: '20%'
},
description: 'Description'
}
}
},
computed: {
componentProps() {
if (!this.component.props) {
return null
}
return Object.keys(this.component.props)
.map(name => {
return this.getPropAttributes(name, this.component.props[name])
})
.sort((a, b) => {
return a.name.localeCompare(b.name)
})
},
componentSlots() {
if (!this.component.slots) {
return null
}
return Object.keys(this.component.slots)
.map(name => {
return {
name: (name.match(/[^/"\\]+/g) || []).join(''),
...this.component.slots[name]
}
})
.sort((a, b) => {
return a.name.localeCompare(b.name)
})
},
componentEvents() {
if (!this.component.events) {
return null
}
return Object.keys(this.component.events)
.map(name => {
return {
name,
...this.component.events[name]
}
})
.sort((a, b) => {
return a.name.localeCompare(b.name)
})
}
},
methods: {
getPropAttributes(name, oldAttributes) {
const attributes = {
name,
...oldAttributes,
...this.getAttributesFromComment(oldAttributes.comment)
}
if (attributes.type && attributes.type.name) {
attributes.types = attributes.type.name.split('|')
}
return attributes
},
getAttributesFromComment(comment) {
const attributes = {}
const optionsMatch = comment.match(/@options[ ]+(\S[ \S]*)\n/)
if (optionsMatch) {
attributes.options = optionsMatch[1].split('|')
}
const defaultMatch = comment.match(/@default[ ]+(\S[ \S]*)\n/)
if (defaultMatch) {
attributes.default = defaultMatch[1]
}
return attributes
}
}
}
</script>
<style lang="scss">
</style>

View File

@ -1,22 +0,0 @@
<template>
<div>
<component-doc :component="component" />
</div>
</template>
<script>
import ComponentDoc from './ComponentDoc'
export default {
name: 'ComponentPage',
props: {
component: {
type: Object,
required: true
}
},
components: {
ComponentDoc
}
}
</script>

Some files were not shown because too many files have changed in this diff Show More