Merge branch 'master' into dependabot/npm_and_yarn/graphql-14.1.1

This commit is contained in:
Grzegorz Leoniec 2019-01-29 03:41:16 +01:00 committed by GitHub
commit a36e2a3e5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1622 additions and 164 deletions

View File

@ -31,7 +31,7 @@ install:
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 > /dev/null
- 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

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developer@human-connection.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,108 +1,127 @@
.tooltip {
display: block !important;
z-index: 10000;
@mixin arrow($size, $type, $color) {
.tooltip-inner {
background: white;
color: $text-color-base;
border-radius: $border-radius-large;
padding: $space-x-small $space-small;
box-shadow: $box-shadow-large;
}
--#{$type}-arrow-size: $size;
.tooltip-arrow {
.#{$type}-arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
border-color: white;
margin: $size;
border-color: $color;
z-index: 1;
}
&[x-placement^="top"] {
margin-bottom: 5px;
margin-bottom: $size;
.tooltip-arrow {
border-width: 5px 5px 0 5px;
.#{$type}-arrow {
border-width: $size $size 0 $size;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: -5px;
left: calc(50% - 5px);
bottom: -$size;
left: calc(50% - var(--#{$type}-arrow-size));
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^="bottom"] {
margin-top: 5px;
margin-top: $size;
.tooltip-arrow {
border-width: 0 5px 5px 5px;
.#{$type}-arrow {
border-width: 0 $size $size $size;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
top: -5px;
left: calc(50% - 5px);
top: -$size;
left: calc(50% - var(--#{$type}-arrow-size));
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^="right"] {
margin-left: 5px;
margin-left: $size;
.tooltip-arrow {
border-width: 5px 5px 5px 0;
.#{$type}-arrow {
border-width: $size $size $size 0;
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
left: -5px;
top: calc(50% - 5px);
left: -$size;
top: calc(50% - var(--#{$type}-arrow-size));
margin-left: 0;
margin-right: 0;
}
}
&[x-placement^="left"] {
margin-right: 5px;
margin-right: $size;
.tooltip-arrow {
border-width: 5px 0 5px 5px;
.#{$type}-arrow {
border-width: $size 0 $size $size;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
right: -5px;
top: calc(50% - 5px);
right: -$size;
top: calc(50% - var(--#{$type}-arrow-size));
margin-left: 0;
margin-right: 0;
}
}
}
.tooltip {
display: block !important;
z-index: $z-index-modal - 2;
.tooltip-inner {
background: $background-color-inverse-soft;
color: $text-color-inverse;
border-radius: $border-radius-base;
padding: $space-x-small $space-small;
box-shadow: $box-shadow-large;
}
@include arrow(5px, "tooltip", $background-color-inverse-soft);
&.popover {
.popover-inner {
background: white;
background: $background-color-soft;
color: $text-color-base;
border-radius: $border-radius-large;
border-radius: $border-radius-base;
padding: $space-x-small $space-small;
box-shadow: $box-shadow-large;
box-shadow: $box-shadow-x-large;
nav {
margin-left: -$space-small;
margin-right: -$space-small;
a {
padding-left: 12px;
}
}
}
.popover-arrow {
border-color: white;
border-color: $background-color-soft;
}
@include arrow(7px, "popover", $background-color-soft);
}
&[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity .15s, visibility .15s;
transition: opacity 60ms;
}
&[aria-hidden='false'] {
visibility: visible;
opacity: 1;
transition: opacity .15s;
transition: opacity 60ms;
}
}

View File

@ -125,6 +125,18 @@ hr {
height: 1px !important;
}
[class$=menu-trigger] {
user-select: none;
}
[class$=menu-popover] {
display: inline-block;
nav {
margin-left: -17px;
margin-right: -15px;
}
}
#overlay {
display: block;
opacity: 0;
@ -142,6 +154,7 @@ hr {
.dropdown-open & {
opacity: 1;
transition-delay: 0;
transition: opacity 80ms ease-out;
}
}
@ -156,6 +169,8 @@ hr {
}
[class$="menu-popover"] {
min-width: 130px;
a, button {
display: flex;
align-content: center;

117
components/ContentMenu.vue Normal file
View File

@ -0,0 +1,117 @@
<template>
<dropdown
class="content-menu"
:placement="placement"
offset="5"
>
<template
slot="default"
slot-scope="{toggleMenu}"
>
<ds-button
class="content-menu-trigger"
size="small"
ghost
@click.prevent="toggleMenu"
>
<ds-icon name="ellipsis-v" />
</ds-button>
</template>
<div
slot="popover"
slot-scope="{toggleMenu}"
class="content-menu-popover"
>
<ds-menu :routes="routes">
<ds-menu-item
slot="menuitem"
slot-scope="item"
:route="item.route"
:parents="item.parents"
@click.stop.prevent="openItem(item.route, toggleMenu)"
>
<ds-icon :name="item.route.icon" />
{{ item.route.name }}
</ds-menu-item>
</ds-menu>
</div>
</dropdown>
</template>
<script>
import Dropdown from '~/components/Dropdown'
export default {
components: {
Dropdown
},
props: {
placement: { type: String, default: 'top-end' },
itemId: { type: String, required: true },
name: { type: String, required: true },
context: {
type: String,
required: true,
validator: value => {
return value.match(/(contribution|comment|organization|user)/)
}
}
},
computed: {
routes() {
let routes = [
{
name: this.$t(`report.${this.context}.title`),
callback: this.openReportDialog,
icon: 'flag'
}
]
if (this.isModerator) {
routes.push({
name: this.$t(`disable.${this.context}.title`),
callback: this.openDisableDialog,
icon: 'eye-slash'
})
}
return routes
},
isModerator() {
return this.$store.getters['auth/isModerator']
}
},
methods: {
openItem(route, toggleMenu) {
if (route.callback) {
route.callback()
} else {
this.$router.push(route.path)
}
toggleMenu()
},
openReportDialog() {
this.$store.commit('modal/SET_OPEN', {
name: 'report',
data: {
context: this.context,
id: this.itemId,
name: this.name
}
})
},
openDisableDialog() {
this.$toast.error('NOT IMPLEMENTED!')
}
}
}
</script>
<style lang="scss">
.content-menu-popover {
nav {
margin-top: -$space-xx-small;
margin-bottom: -$space-xx-small;
margin-left: -$space-x-small;
margin-right: -$space-x-small;
}
}
</style>

View File

@ -1,68 +1,77 @@
<template>
<a
v-router-link
:href="href(post)"
<ds-card
:header="post.title"
:image="post.image"
class="post-card"
>
<ds-card
:header="post.title"
:image="post.image"
style="cursor: pointer; position: relative;"
<a
v-router-link
class="post-link"
:href="href(post)"
>
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<ds-space margin-bottom="large">
<div
class="hc-editor-content"
v-html="excerpt"
{{ post.title }}
</a>
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<ds-space margin-bottom="large">
<div
class="hc-editor-content"
v-html="excerpt"
/>
</ds-space>
<!-- eslint-enable vue/no-v-html -->
<ds-space
margin="small"
style="position: absolute; bottom: 44px; z-index: 1;"
>
<!-- TODO: find better solution for rendering errors -->
<no-ssr>
<hc-author
:post="post"
:trunc="35"
:show-author-popover="showAuthorPopover"
/>
</ds-space>
<!-- eslint-enable vue/no-v-html -->
<ds-space
margin="small"
style="position: absolute; bottom: 44px;"
>
<!-- TODO: find better solution for rendering errors -->
</no-ssr>
</ds-space>
<template slot="footer">
<div style="display: inline-block; opacity: .5;">
<ds-icon
v-for="category in post.categories"
:key="category.id"
v-tooltip="{content: category.name, placement: 'bottom-start', delay: { show: 500 }}"
:name="category.icon"
/>&nbsp;
</div>
<div style="display: inline-block; float: right">
<span :style="{ opacity: post.shoutedCount ? 1 : .5 }">
<ds-icon name="bullhorn" /> <small>{{ post.shoutedCount }}</small>
</span>
&nbsp;
<span :style="{ opacity: post.commentsCount ? 1 : .5 }">
<ds-icon name="comments" /> <small>{{ post.commentsCount }}</small>
</span>
<no-ssr>
<hc-author
:post="post"
:trunc="35"
:show-author-popover="showAuthorPopover"
<content-menu
context="contribution"
:item-id="post.id"
:name="post.title"
/>
</no-ssr>
</ds-space>
<template slot="footer">
<div style="display: inline-block; opacity: .5;">
<ds-icon
v-for="category in post.categories"
:key="category.id"
v-tooltip="{content: category.name, placement: 'bottom-start', delay: { show: 500 }}"
:name="category.icon"
/>&nbsp;
</div>
<div style="display: inline-block; float: right">
<span :style="{ opacity: post.shoutedCount ? 1 : .5 }">
<ds-icon name="bullhorn" />
<small>{{ post.shoutedCount }}</small>
</span>
&nbsp;
<span :style="{ opacity: post.commentsCount ? 1 : .5 }">
<ds-icon name="comments" />
<small>{{ post.commentsCount }}</small>
</span>
</div>
</template>
</ds-card>
</a>
</div>
</template>
</ds-card>
</template>
<script>
import HcAuthor from '~/components/Author.vue'
import ContentMenu from '~/components/ContentMenu'
import { randomBytes } from 'crypto'
export default {
name: 'HcPostCard',
components: {
HcAuthor
HcAuthor,
ContentMenu
},
props: {
post: {
@ -96,3 +105,31 @@ export default {
}
}
</script>
<style lang="scss">
.post-card {
cursor: pointer;
position: relative;
.ds-card-footer {
z-index: 1;
}
.content-menu {
display: inline-block;
margin-left: $space-xx-small;
margin-right: -$space-x-small;
z-index: 1;
}
}
.post-link {
display: block;
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-indent: -999999px;
}
</style>

144
components/ReportModal.vue Normal file
View File

@ -0,0 +1,144 @@
<template>
<ds-modal
:title="title"
:is-open="isOpen"
:confirm-label="$t('report.submit')"
:cancel-label="$t('report.cancel')"
confrim-icon="warning"
@confirm="report"
@cancel="close"
>
<transition name="ds-transition-fade">
<ds-flex
v-if="success"
class="hc-modal-success"
centered
>
<sweetalert-icon icon="success" />
</ds-flex>
</transition>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$t(`report.${data.context}.message`, { name: name })" />
<template
slot="footer"
slot-scope="{ cancel, confirm, cancelLabel, confirmLabel }"
>
<ds-button
ghost
icon="close"
:disabled="disabled || loading"
@click.prevent="cancel('cancel')"
>
{{ cancelLabel }}
</ds-button>
<ds-button
danger
icon="exclamation-circle"
:loading="loading"
:disabled="disabled || loading"
@click.prevent="confirm('confirm')"
>
{{ confirmLabel }}
</ds-button>
</template>
</ds-modal>
</template>
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
name: 'ReportModal',
components: {
SweetalertIcon
},
data() {
return {
success: false,
loading: false,
disabled: false
}
},
computed: {
data() {
return this.$store.getters['modal/data'] || {}
},
title() {
return this.$t(`report.${this.data.context}.title`)
},
name() {
return this.$filters.truncate(this.data.name, 30)
},
isOpen() {
return this.$store.getters['modal/open'] === 'report'
}
},
watch: {
isOpen(open) {
if (open) {
this.success = false
this.disabled = false
this.loading = false
}
}
},
methods: {
close() {
this.$store.commit('modal/SET_OPEN', {})
},
report() {
console.log('')
this.loading = true
this.disabled = true
this.$apollo
.mutate({
mutation: gql`
mutation($id: ID!, $type: ResourceEnum!, $description: String) {
report(
resource: { id: $id, type: $type }
description: $description
) {
id
}
}
`,
variables: {
id: this.data.id,
type: this.data.context,
description: '-'
}
})
.then(() => {
this.success = true
this.$toast.success('Thanks for reporting!')
setTimeout(this.close, 1500)
})
.catch(err => {
this.$toast.error(err.message)
this.disabled = false
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
<style lang="scss">
.hc-modal-success {
pointer-events: none;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: #fff;
opacity: 1;
z-index: $z-index-modal;
border-radius: $border-radius-x-large;
}
</style>

View File

@ -0,0 +1,17 @@
{
"admin": {
"email": "admin@example.org",
"password": "1234",
"name": "Peter Lustig"
},
"moderator": {
"email": "moderator@example.org",
"password": "1234",
"name": "Bob der Bausmeister"
},
"user": {
"email": "user@example.org",
"password": "1234",
"name": "Jenny Rostock"
}
}

View File

@ -1,4 +1,4 @@
Feature: About me and and location
Feature: About me and location
As a user
I would like to add some about me text and a location
So others can get some info about me and my location

View File

@ -0,0 +1,59 @@
Feature: Report and Moderate
As a user
I would like to report content that viloates the community guidlines
So the moderators can take action on it
As a moderator
I would like to see all reported content
So I can look into it and decide what to do
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 |
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
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"?
"""
Then I see a success message:
"""
Thanks for reporting!
"""
Examples:
| Page |
| landing page |
| post page |
Scenario: Report user
Given I am logged in with a "user" role
And I see David Irving's post on the post page
When I click on the author
And I click on "Report User" from the triple dot menu in the user info box
And I confirm the reporting dialog because he is a holocaust denier:
"""
Do you really want to report the user "David Irving"?
"""
Then I see a success message:
"""
Thanks for reporting!
"""
Scenario: Review reported content
Given somebody reported the following posts:
| Slug |
| the-truth-about-the-holocaust |
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"
Then I see all the reported posts including the one from above
And each list item links to the post page
Scenario: Normal user can't see the moderation page
Given I am logged in with a "user" role
When I click on the avatar menu in the top right corner
Then I can't see the moderation menu item

View File

@ -0,0 +1,141 @@
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'
const savePostTitle = $post => {
return $post
.first()
.find('.ds-heading')
.first()
.invoke('text')
.then(title => {
lastReportTitle = title
})
}
Given("I see David Irving's post on the landing page", page => {
cy.openPage('landing')
})
Given("I see David Irving's post on the post page", page => {
cy.visit(`/post/${dummyReportedPostSlug}`)
cy.contains(dummyReportedPostTitle) // wait
})
Given('I am logged in with a {string} role', role => {
cy.loginAs(role)
})
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)
.find('.content-menu-trigger')
.first()
.click()
cy.get('.popover .ds-menu-item-link')
.contains('Report Contribution')
.click()
}
)
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)
.find('.content-menu-trigger')
.first()
.click()
cy.get('.popover .ds-menu-item-link')
.contains('Report User')
.click()
}
)
When('I click on the author', () => {
cy.get('a.author')
.first()
.click()
.wait(200)
})
When('I report the author', () => {
cy.get('.page-name-profile-slug').then(() => {
invokeReportOnElement('.ds-card').then(() => {
cy.get('button')
.contains('Send')
.click()
})
})
})
When('I click on send in the confirmation dialog', () => {
cy.get('button')
.contains('Send')
.click()
})
Then('I get a success message', () => {
cy.get('.iziToast-message').contains('Thanks')
})
Then('I see my reported user', () => {
cy.get('table').then(() => {
cy.get('tbody tr')
.first()
.contains(lastReportTitle.trim())
})
})
Then(`I can't see the moderation menu item`, () => {
cy.get('.avatar-menu-popover')
.find('a[href="/settings"]', 'Settings')
.should('exist') // OK, the dropdown is actually open
cy.get('.avatar-menu-popover')
.find('a[href="/moderation"]', 'Moderation')
.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'
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')
.click()
})
})
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)
})
})
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)
})
})
Then('each list item links to the post page', () => {
//TODO: match the right post
cy.contains(dummyReportedPostTitle).click()
cy.location('pathname').should('contain', '/post')
})

View File

@ -1,20 +1,14 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { getLangByName } from '../../support/helpers'
import find from 'lodash/find'
import users from '../../fixtures/users.json'
/* global cy */
const username = 'Peter Lustig'
const openPage = page => {
if (page === 'landing') {
page = ''
}
cy.visit(`/${page}`)
}
Given('I am logged in', () => {
cy.login('admin@example.org', 1234)
cy.loginAs('admin')
})
Given('I am logged in as {string}', userType => {
cy.loginAs(userType)
})
Given('we have a selection of tags and categories as well as posts', () => {
@ -32,10 +26,11 @@ Given('my user account has the role {string}', role => {
When('I log out', cy.logout)
When('I visit the {string} page', page => {
openPage(page)
cy.openPage(page)
})
Given('I am on the {string} page', page => {
openPage(page)
cy.openPage(page)
})
When('I fill in my email and password combination and click submit', () => {
@ -53,22 +48,22 @@ When('I log out through the menu in the top right corner', () => {
.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)
cy.get('.avatar-menu-popover').should('contain', users.admin.name)
})
Then('I see the login screen again', () => {
cy.location('pathname').should('contain', '/login')
})
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 am still logged in', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover').contains(username)
cy.get('.avatar-menu-popover').contains(users.admin.name)
})
When('I select {string} in the language menu', name => {
@ -93,3 +88,18 @@ When(`I click on {string}`, linkOrButton => {
When('I press {string}', label => {
cy.contains(label).click()
})
Given('we have the following posts in our database:', table => {
table.hashes().forEach(row => {
//TODO: calll factory here
//create('post', row)
})
})
Then('I see a success message:', message => {
cy.contains(message)
})
When('I click on the avatar menu in the top right corner', () => {
cy.get('.avatar-menu').click()
})

View File

@ -15,6 +15,7 @@
/* globals Cypress cy */
import { getLangByName } from './helpers'
import users from '../fixtures/users.json'
const switchLang = name => {
cy.get('.locale-menu').click()
@ -54,11 +55,24 @@ Cypress.Commands.add('login', (email, password) => {
.click()
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
})
Cypress.Commands.add('openPage', page => {
if (page === 'landing') {
page = ''
}
cy.visit(`/${page}`)
})
//
//
// -- This is a child command --

View File

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

View File

@ -1,4 +1,3 @@
import find from 'lodash/find'
const helpers = {

View File

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

View File

@ -0,0 +1,41 @@
import gql from 'graphql-tag'
export default app => {
return gql(`
query {
Report(first: 20, orderBy: createdAt_desc) {
id
description
type
createdAt
reporter {
name
slug
}
user {
name
slug
}
comment {
contentExcerpt
author {
name
slug
}
post {
title
slug
}
}
contribution {
title
slug
author {
name
slug
}
}
}
}
`)
}

View File

@ -92,6 +92,12 @@
</div>
</ds-container>
<div id="overlay" />
<no-ssr>
<portal-target name="modal" />
</no-ssr>
<no-ssr>
<report-modal />
</no-ssr>
</div>
</template>
@ -99,11 +105,13 @@
import { mapGetters } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch'
import Dropdown from '~/components/Dropdown'
import ReportModal from '~/components/ReportModal'
import seo from '~/components/mixins/seo'
export default {
components: {
Dropdown,
ReportModal,
LocaleSwitch
},
mixins: [seo],
@ -111,6 +119,7 @@ export default {
...mapGetters({
user: 'auth/user',
isLoggedIn: 'auth/isLoggedIn',
isModerator: 'auth/isModerator',
isAdmin: 'auth/isAdmin'
}),
routes() {
@ -129,6 +138,13 @@ export default {
icon: 'cogs'
}
]
if (this.isModerator) {
routes.push({
name: this.$t('moderation.name'),
path: `/moderation`,
icon: 'balance-scale'
})
}
if (this.isAdmin) {
routes.push({
name: this.$t('admin.name'),
@ -178,8 +194,8 @@ export default {
align-items: center;
padding-left: $space-xx-small;
}
.avatar-menu-popover {
display: inline-block;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@ -191,8 +207,17 @@ export default {
.logout-link {
margin-left: -$space-small;
margin-right: -$space-small;
margin-bottom: -$space-xx-small;
padding: $space-xx-small $space-small;
margin-top: -$space-xxx-small;
margin-bottom: -$space-x-small;
padding: $space-x-small $space-small;
// subtract menu border with from padding
padding-left: $space-small - 2;
color: $text-color-base;
&:hover {
color: $text-color-link-active;
}
}
nav {
@ -200,8 +225,6 @@ export default {
margin-right: -$space-small;
margin-top: -$space-xx-small;
margin-bottom: -$space-xx-small;
// padding-top: $space-xx-small;
// padding-bottom: $space-xx-small;
a {
padding-left: 12px;

View File

@ -92,6 +92,31 @@
"name": "Einstellungen"
}
},
"moderation": {
"name": "Moderation",
"reports": {
"empty": "Glückwunsch, es gibt nichts zu moderieren.",
"name": "Meldungen",
"reporter": "gemeldet von"
}
},
"disable": {
"user": {
"title": "Nutzer sperren",
"type": "Nutzer",
"message": "Bist du sicher, dass du den Nutzer \"<b>{name}</b>\" deaktivieren möchtest?"
},
"contribution": {
"title": "Beitrag sperren",
"type": "Beitrag",
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" deaktivieren möchtest?"
},
"comment": {
"title": "Kommentar sperren",
"type": "Kommentar",
"message": "Bist du sicher, dass du den Kommentar \"<b>{name}</b>\" deaktivieren möchtest?"
}
},
"post": {
"name": "Beitrag",
"moreInfo": {
@ -101,6 +126,25 @@
"name": "Aktiv werden"
}
},
"report": {
"submit": "Meldung senden",
"cancel": "Abbrechen",
"user": {
"title": "Nutzer melden",
"type": "Nutzer",
"message": "Bist du sicher, dass du den Nutzer \"<b>{name}</b>\" melden möchtest?"
},
"contribution": {
"title": "Beitrag melden",
"type": "Beitrag",
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" melden möchtest?"
},
"comment": {
"title": "Kommentar melden",
"type": "Kommentar",
"message": "Bist du sicher, dass du den Kommentar von \"<b>{name}</b>\" melden möchtest?"
}
},
"quotes": {
"african": {
"quote": "Viele kleine Leute, an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.",
@ -108,6 +152,7 @@
}
},
"common": {
"reportContent": "Melden",
"post": "Beitrag ::: Beiträge",
"comment": "Kommentar ::: Kommentare",
"letsTalk": "Miteinander reden",

View File

@ -92,6 +92,31 @@
"name": "Settings"
}
},
"moderation": {
"name": "Moderation",
"reports": {
"empty": "Congratulations, nothing to moderate.",
"name": "Reports",
"reporter": "reported by"
}
},
"disable": {
"user": {
"title": "Disable User",
"type": "User",
"message": "Do you really want to disable the user \"<b>{name}</b>\"?"
},
"contribution": {
"title": "Disable Contribution",
"type": "Contribution",
"message": "Do you really want to disable the contribution \"<b>{name}</b>\"?"
},
"comment": {
"title": "Disable Comment",
"type": "Comment",
"message": "Do you really want to disable the comment from \"<b>{name}</b>\"?"
}
},
"post": {
"name": "Post",
"moreInfo": {
@ -101,6 +126,25 @@
"name": "Take action"
}
},
"report": {
"submit": "Send Report",
"cancel": "Cancel",
"user": {
"title": "Report User",
"type": "User",
"message": "Do you really want to report the user \"<b>{name}</b>\"?"
},
"contribution": {
"title": "Report Contribution",
"type": "Contribution",
"message": "Do you really want to report the contribution \"<b>{name}</b>\"?"
},
"comment": {
"title": "Report Comment",
"type": "Comment",
"message": "Do you really want to report the comment from \"<b>{name}</b>\"?"
}
},
"quotes": {
"african": {
"quote": "Many small people in many small places do many small things, that can alter the face of the world.",
@ -108,6 +152,7 @@
}
},
"common": {
"reportContent": "Report",
"post": "Post ::: Posts",
"comment": "Comment ::: Comments",
"letsTalk": "Let`s Talk",

5
middleware/isAdmin.js Normal file
View File

@ -0,0 +1,5 @@
export default ({ store, error }) => {
if (!store.getters['auth/isAdmin']) {
return error({ statusCode: 403 })
}
}

View File

@ -0,0 +1,5 @@
export default ({ store, error }) => {
if (!store.getters['auth/isModerator']) {
return error({ statusCode: 403 })
}
}

View File

@ -74,7 +74,10 @@ module.exports = {
router: {
middleware: ['authenticated'],
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active'
linkExactActiveClass: 'router-link-exact-active',
scrollBehavior: () => {
return { x: 0, y: 0 }
}
},
/*
@ -86,6 +89,7 @@ module.exports = {
'cookie-universal-nuxt',
'@nuxtjs/apollo',
'@nuxtjs/axios',
'portal-vue/nuxt',
[
'nuxt-sass-resources-loader',
path.resolve(__dirname, './styleguide/src/system/styles/shared.scss')

View File

@ -52,9 +52,11 @@
"nuxt": "^2.0.0",
"nuxt-env": "^0.0.4",
"nuxt-sass-resources-loader": "^2.0.5",
"portal-vue": "~1.5.1",
"v-tooltip": "^2.0.0-rc.33",
"vue-count-to": "^1.0.13",
"vue-izitoast": "1.1.2",
"vue-sweetalert-icons": "^3.2.0",
"vuex-i18n": "^1.11.0"
},
"devDependencies": {

View File

@ -4,13 +4,13 @@
{{ $t('admin.name') }}
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }">
<ds-flex-item :width="{ base: '100%', md: '200px' }">
<ds-menu
:routes="routes"
:is-exact="() => true"
/>
</ds-flex-item>
<ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
<transition
name="slide-up"
appear
@ -24,6 +24,7 @@
<script>
export default {
middleware: ['isAdmin'],
computed: {
routes() {
return [

36
pages/moderation.vue Normal file
View File

@ -0,0 +1,36 @@
<template>
<div>
<ds-heading tag="h1">
{{ $t('moderation.name') }}
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', md: '200px' }">
<ds-menu :routes="routes" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
<transition
name="slide-up"
appear
>
<nuxt-child />
</transition>
</ds-flex-item>
</ds-flex>
</div>
</template>
<script>
export default {
middleware: ['isModerator'],
computed: {
routes() {
return [
{
name: this.$t('moderation.reports.name'),
path: `/moderation`
}
]
}
}
}
</script>

116
pages/moderation/index.vue Normal file
View File

@ -0,0 +1,116 @@
<template>
<ds-card space="small">
<ds-heading tag="h3">
{{ $t('moderation.reports.name') }}
</ds-heading>
<ds-table
v-if="Report && Report.length"
:data="Report"
:fields="fields"
condensed
>
<template
slot="name"
slot-scope="scope"
>
<div v-if="scope.row.type === 'contribution'">
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.contribution.slug } }">
<b>{{ scope.row.contribution.title | truncate(50) }}</b>
</nuxt-link><br>
<ds-text
size="small"
color="soft"
>
{{ scope.row.contribution.author.name }}
</ds-text>
</div>
<div v-else-if="scope.row.type === 'comment'">
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.comment.post.slug } }">
<b>{{ scope.row.comment.contentExcerpt | truncate(50) }}</b>
</nuxt-link><br>
<ds-text
size="small"
color="soft"
>
{{ scope.row.comment.author.name }}
</ds-text>
</div>
<div v-else>
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.user.slug } }">
<b>{{ scope.row.user.name | truncate(50) }}</b>
</nuxt-link>
</div>
</template>
<template
slot="type"
slot-scope="scope"
>
<ds-text
color="soft"
>
<ds-icon
v-if="scope.row.type === 'contribution'"
v-tooltip="{ content: $t(`report.${scope.row.type}.type`), placement: 'right' }"
name="bookmark"
/>
<ds-icon
v-else-if="scope.row.type === 'comment'"
v-tooltip="{ content: $t(`report.${scope.row.type}.type`), placement: 'right' }"
name="comments"
/>
<ds-icon
v-else
v-tooltip="{ content: $t(`report.${scope.row.type}.type`), placement: 'right' }"
name="user"
/>
</ds-text>
</template>
<template
slot="reporter"
slot-scope="scope"
>
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.reporter.slug } }">
{{ scope.row.reporter.name }}
</nuxt-link>
</template>
</ds-table>
<hc-empty
v-else
icon="alert"
:message="$t('moderation.reports.empty')"
/>
</ds-card>
</template>
<script>
import gql from 'graphql-tag'
import HcEmpty from '~/components/Empty.vue'
import query from '~/graphql/ModerationListQuery.js'
export default {
components: {
HcEmpty
},
data() {
return {
Report: []
}
},
computed: {
fields() {
return {
type: ' ',
name: ' ',
reporter: this.$t('moderation.reports.reporter')
// actions: ' '
}
}
},
apollo: {
Report: {
query,
fetchPolicy: 'cache-and-network'
}
}
}
</script>

View File

@ -6,6 +6,14 @@
class="post-card"
>
<hc-author :post="post" />
<no-ssr>
<content-menu
placement="bottom-end"
context="contribution"
:item-id="post.id"
:name="post.title"
/>
</no-ssr>
<ds-space margin-bottom="small" />
<!-- Content -->
<!-- eslint-disable vue/no-v-html -->
@ -82,6 +90,15 @@
<ds-space margin-bottom="x-small">
<hc-author :post="comment" />
</ds-space>
<no-ssr>
<content-menu
placement="bottom-end"
context="comment"
style="float-right"
:item-id="comment.id"
:name="comment.author.name"
/>
</no-ssr>
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<div
@ -110,6 +127,7 @@
<script>
import gql from 'graphql-tag'
import ContentMenu from '~/components/ContentMenu'
import HcAuthor from '~/components/Author.vue'
import HcShoutButton from '~/components/ShoutButton.vue'
import HcEmpty from '~/components/Empty.vue'
@ -122,7 +140,8 @@ export default {
components: {
HcAuthor,
HcShoutButton,
HcEmpty
HcEmpty,
ContentMenu
},
head() {
return {
@ -221,28 +240,41 @@ export default {
</script>
<style lang="scss">
.post-card {
// max-width: 800px;
margin: auto;
.page-name-post-slug {
.content-menu {
float: right;
margin-right: -$space-x-small;
margin-top: -$space-large;
}
.comments {
margin-top: $space-small;
.post-card {
// max-width: 800px;
margin: auto;
.comment {
.comments {
margin-top: $space-small;
position: relative;
}
}
.ds-card-image {
img {
max-height: 300px;
object-fit: cover;
object-position: center;
.comment {
margin-top: $space-small;
position: relative;
}
}
.ds-card-image {
img {
max-height: 300px;
object-fit: cover;
object-position: center;
}
}
.ds-card-footer {
padding: 0;
.ds-section {
padding: $space-base;
}
}
}
.ds-card-footer {
padding-bottom: 0;
}
}
</style>

View File

@ -17,6 +17,14 @@
class="profile-avatar"
size="120px"
/>
<no-ssr>
<content-menu
placement="bottom-end"
context="user"
:item-id="user.id"
:name="user.name"
/>
</no-ssr>
<ds-space margin="small">
<ds-heading
tag="h3"
@ -284,6 +292,7 @@ import HcCountTo from '~/components/CountTo.vue'
import HcBadges from '~/components/Badges.vue'
import HcLoadMore from '~/components/LoadMore.vue'
import HcEmpty from '~/components/Empty.vue'
import ContentMenu from '~/components/ContentMenu'
export default {
components: {
@ -293,7 +302,8 @@ export default {
HcCountTo,
HcBadges,
HcLoadMore,
HcEmpty
HcEmpty,
ContentMenu
},
transition: {
name: 'slide-up',
@ -402,6 +412,14 @@ export default {
border: #fff 5px solid;
}
.page-name-profile-slug {
.ds-flex-item:first-child .content-menu {
position: absolute;
top: $space-x-small;
right: $space-x-small;
}
}
.profile-top-navigation {
position: sticky;
top: 53px;
@ -416,10 +434,10 @@ export default {
&.ds-tab-nav-item-active {
border-bottom: 3px solid #17b53f;
&:first-child {
border-bottom-left-radius: $border-radius-large;
border-bottom-left-radius: $border-radius-x-large;
}
&:last-child {
border-bottom-right-radius: $border-radius-large;
border-bottom-right-radius: $border-radius-x-large;
}
}
}

View File

@ -4,13 +4,13 @@
{{ $t('settings.name') }}
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }">
<ds-flex-item :width="{ base: '100%', md: '200px' }">
<ds-menu
:routes="routes"
:is-exact="() => true"
/>
</ds-flex-item>
<ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
<transition
name="slide-up"
appear

22
store/modal.js Normal file
View File

@ -0,0 +1,22 @@
export const state = () => {
return {
open: null,
data: {}
}
}
export const mutations = {
SET_OPEN(state, ctx) {
state.open = ctx.name || null
state.data = ctx.data || {}
}
}
export const getters = {
open(state) {
return state.open
},
data(state) {
return state.data
}
}

View File

@ -14,6 +14,7 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"portal-vue": "^1.5.1",
"vue": "^2.5.17"
},
"devDependencies": {

View File

@ -1,4 +1,4 @@
$border-radius: $border-radius-large;
$border-radius: $border-radius-x-large;
.ds-card {
@include reset;
@ -7,7 +7,7 @@ $border-radius: $border-radius-large;
flex-direction: column;
background-color: $background-color-base;
color: $text-color-base;
box-shadow: $box-shadow-large;
box-shadow: $box-shadow-base;
height: 100%;
}

View File

@ -0,0 +1,185 @@
<template>
<div>
<portal to="modal">
<div :key="key" class="ds-modal-wrapper">
<transition name="ds-transition-fade" appear>
<div
v-if="isOpen"
class="ds-modal-backdrop"
ref="backdrop"
@click="backdropHandler"
>
&nbsp;
</div>
</transition>
<transition name="ds-transition-modal-appear" appear>
<ds-card
v-if="isOpen"
class="ds-modal"
:class="[extended && 'ds-modal-extended']"
:header="title"
tableindex="-1"
role="dialog"
ref="modal"
style="display: block"
>
<ds-button
v-if="!force"
class="ds-modal-close"
ghost
size="small"
icon="close"
aria-hidden="true"
@click="cancel('close')"
/>
<!-- @slot Modal content -->
<slot ref="modalBody"/>
<template slot="footer">
<!-- @slot Modal footer with action buttons -->
<slot
name="footer"
:confirm="confirm"
:cancel="cancel"
:cancelLabel="cancelLabel"
:confirmLabel="confirmLabel"
>
<ds-button ghost icon="close" @click.prevent="cancel('cancel')">{{ cancelLabel }}</ds-button>
<ds-button primary icon="check" @click.prevent="confirm('confirm')">{{ confirmLabel }}</ds-button>
</slot>
</template>
</ds-card>
</transition>
</div>
</portal>
</div>
</template>
<script>
import Vue from 'vue'
import portal from 'portal-vue'
Vue.use(portal)
/* eslint-disable no-empty */
/**
* Simple Modal Component
* @version 1.0.0
*/
export default {
name: 'DsModal',
props: {
/**
* Modal title
*/
title: {
type: String,
default: null
},
/**
* Open state
*/
isOpen: {
type: Boolean,
default: false
},
/**
* Force user input by disabeling the ESC key, close button and click on the backdrop
*/
force: {
type: Boolean,
default: false
},
/**
* Allow closing without choosing action by ESC key, close button or click on the backdrop
*/
extended: {
type: Boolean,
default: false
},
/**
* Cancel button label
*/
cancelLabel: {
type: String,
default: 'Cancel'
},
/**
* Confirm button label
*/
confirmLabel: {
type: String,
default: 'Confirm'
}
},
model: {
prop: 'isOpen',
event: 'update:isOpen'
},
watch: {
isOpen: {
immediate: true,
handler(show) {
try {
if (show) {
this.$emit('opened')
document
.getElementsByTagName('body')[0]
.classList
.add('modal-open')
} else {
document
.getElementsByTagName('body')[0]
.classList
.remove('modal-open')
}
} catch (err) {}
}
}
},
methods: {
confirm (type = 'confirm') {
this.$emit('confirm')
this.close(type)
},
cancel (type = 'cancel') {
this.$emit('cancel')
this.close(type)
},
close (type) {
this.$emit('update:isOpen', false)
this.$emit('close', type)
},
backdropHandler () {
if (!this.force) {
this.cancel('backdrop')
}
}
},
beforeCreate() {
// create random key string
this.key = Math.random()
.toString(36)
.replace(/[^a-z]+/g, '')
.substr(0, 5)
},
mounted() {
const keydownListener = document.addEventListener('keydown', e => {
if (this.isOpen && !this.force && e.keyCode === 27) {
this.cancel('backdrop')
}
})
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keydown', keydownListener)
})
if (this.isOpen) {
this.$emit('opened')
}
}
}
</script>
<style lang="scss" src="./style.scss">
</style>
<docs src="./demo.md"></docs>

View File

@ -0,0 +1,66 @@
## Basic Modal
Basic modal usage
You will need to add the portal-target to the end of your html body to get the modal working properly
```html
<!-- put the following tag as last element to your html body / layout -->
<!-- make sure you only include it once! -->
<portal-target name="modal" style="position: absolute" />
```
```
<template>
<div>
<ds-modal
v-model="isOpen"
title="Modal Title"
>
<p>Hello World</p>
</ds-modal>
<ds-button primary icon="rocket" @click="isOpen = true">Open Modal</ds-button>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: false
}
}
}
</script>
```
Customize button labels
```
<template>
<div>
<ds-modal
v-if="isOpen"
v-model="isOpen"
title="Custom Button Labels"
force
extended
confirm-label="All right"
cancel-label="Please not"
>
<p>Culpa amet sunt aperiam ratione est sed. Molestiae minus doloremque libero. Beatae nam repellendus aliquid maxime.</p>
<p>Sint quasi provident natus id earum debitis. Et facilis a iure ullam. Velit autem eveniet ea reprehenderit ducimus doloribus earum quo.</p>
<p>Consequatur ratione repudiandae aliquid ea. Ut eum architecto assumenda. Autem eaque provident quia et.</p>
<p>Eaque quia aut dolorum sunt ea consequuntur. Labore reprehenderit placeat pariatur molestiae sit laborum nostrum. Deserunt est commodi et suscipit tenetur ipsa voluptas cupiditate. Porro laborum quidem ut corrupti. Dolorum et est placeat qui.</p>
<p>Adipisci beatae cumque esse harum. Error quis nulla illo nemo est. Enim est quis explicabo voluptatem. Omnis maxime qui similique consequatur voluptatibus. Est necessitatibus iure aliquid omnis eum. Ut voluptatibus vel error exercitationem temporibus qui expedita.</p>
</ds-modal>
<ds-button primary icon="rocket" @click="isOpen = true">Open Modal</ds-button>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: false
}
}
}
</script>
```

View File

@ -0,0 +1,100 @@
.ds-modal-wrapper {
padding: $space-base;
position: relative;
}
.ds-modal {
position: fixed;
z-index: $z-index-modal;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
display: flex;
flex-direction: column;
max-width: 400px;
width: calc(90vw - 40px);
height: auto;
max-height: 90vh;
box-shadow: $box-shadow-x-large;
&.ds-modal-extended {
max-width: 600px;
}
}
.ds-modal .ds-card-header {
position: relative;
&::after {
content: "";
height: 30px;
background: linear-gradient(rgba(255,255,255,1), rgba(255,255,255,0));
position: absolute;
width: calc(100% - 10px);
bottom: -30px;
left: 0;
pointer-events: none;
z-index: 1;
}
}
.ds-modal-close {
position: absolute;
top: $space-small;
right: $space-small;
}
.ds-modal .ds-card-content {
flex: 1;
overflow-y: auto;
height: auto;
min-height: 50px;
max-height: 50vh;
padding-bottom: $space-large !important;
}
.ds-modal footer {
position: relative;
display: flex;
overflow: visible;
flex-shrink: 0;
justify-content: flex-end;
background-color: $background-color-softer;
padding: $space-small;
& > button {
margin-left: $space-x-small;
}
&::before {
content: "";
height: 45px;
background: linear-gradient(rgba(255,255,255,0), rgba(255,255,255,.9));
position: absolute;
width: calc(100% - 10px);
z-index: 1;
left: 0;
top: -45px;
pointer-events: none;
}
}
.ds-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: $z-index-modal - 1;
background: rgba(0, 0, 0, 0.7);
}
.ds-transition-modal-appear-enter-active {
opacity: 1;
transition: all 200ms $ease-out-bounce;
transform: translate3d(-50%, -50%, 0) scale(1);
}
.ds-transition-modal-appear-enter,
.ds-transition-modal-appear-leave-active {
opacity: 0;
transform: translate3d(-50%, -50%, 0) scale(0.8);
}

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>flag</title>
<path d="M5 5h12v14h-10v10h-2v-24zM18 8h9v14h-9v-14z"></path>
</svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@ -16,3 +16,5 @@
@import "./shared/background";
@import "./shared/spacing";
@import "./shared/form";
@import "./shared/transitions";
@import "./shared/animations";

View File

@ -0,0 +1,19 @@
@keyframes ds-animation-shake {
from, to {
transform: translate3d(0, 0, 0);
}
10%, 30%, 50%, 70%, 90% {
transform: translate3d(-5px, 0, 0);
}
20%, 40%, 60%, 80% {
transform: translate3d(5px, 0, 0);
}
}
.ds-animated {
animation-duration: 0.8s;
animation-fill-mode: both;
}
.ds-animation-shake {
animation-name: ds-animation-shake;
}

View File

@ -0,0 +1,66 @@
$easeOut: cubic-bezier(0.19, 1, 0.22, 1);
// slide up ease
.ds-transition-slide-up-enter-active {
transition: all 500ms $easeOut;
transition-delay: 20ms;
opacity: 1;
transform: translateY(0);
}
.ds-transition-slide-up-enter,
.ds-transition-slide-up-leave-active {
opacity: 0;
box-shadow: none;
transform: translateY(15px);
}
// slide next / prev
.ds-transition-slide-next-enter-active,
.ds-transition-slide-prev-enter-active {
transition: transform 500ms $easeOut, opacity 500ms $easeOut;
transition-delay: 100ms;
opacity: 1;
}
.ds-transition-slide-next-enter,
.ds-transition-slide-next-leave-active {
opacity: 0;
transform: translateX(10px);
}
.ds-transition-slide-prev-enter,
.ds-transition-slide-prev-leave-active {
opacity: 0;
transform: translateX(-10px);
}
.ds-transition-slide-next-leave-active,
.ds-transition-slide-prev-leave-active {
display: none;
}
.ds-transition-slide-next-leave-active,
.ds-transition-slide-prev-leave-active {
opacity: 0;
transform: translateX(-2px);
transition: transform 100ms $easeOut, opacity 100ms $easeOut;
}
.ds-transition-fade-delayed-leave-active {
transition: opacity 0ms;
transition-delay: 0ms;
}
.ds-transition-fade-delayed-enter-active {
transition: opacity 300ms ease-out;
transition-delay: 100ms;
opacity: 1;
}
.ds-transition-fade-delayed-enter,
.ds-transition-fade-delayed-leave-active {
opacity: 0.1;
}
.ds-transition-fade-enter-active,
.ds-transition-fade-leave-active {
transition: opacity 200ms;
}
.ds-transition-fade-enter,
.ds-transition-fade-leave-to {
opacity: 0;
}

View File

@ -5,6 +5,8 @@
#
props:
- name: box-shadow-x-large
value: "0 40px 120px 0 rgba(0, 0, 0, .8)"
- name: box-shadow-large
value: "0 20px 60px 0 rgba(0, 0, 0, .8)"
- name: box-shadow-base

View File

@ -5,11 +5,11 @@
props:
- name: border-radius-x-large
value: "8px"
value: "5px"
- name: border-radius-large
value: "6px"
value: "4px"
- name: border-radius-base
value: "3px"
value: "4px"
- name: border-radius-rounded
value: "2em"
- name: border-radius-circle

View File

@ -8039,6 +8039,11 @@ pn@^1.1.0:
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
portal-vue@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-1.5.1.tgz#6bed79ef168d9676bb79f41d43c5cd4cedf54dbc"
integrity sha512-7T0K+qyY8bnjnEpQTiLbGsUaGlFcemK9gLurVSr6x1/qzr2HkHDNCOz5i+xhuTD1CrXckf/AGeCnLzvmAHMOHw==
portfinder@^1.0.19, portfinder@^1.0.9:
version "1.0.19"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.19.tgz#07e87914a55242dcda5b833d42f018d6875b595f"

View File

@ -8755,6 +8755,11 @@ popper.js@^1.12.9:
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.5.tgz#98abcce7c7c34c4ee47fcbc6b3da8af2c0a127bc"
integrity sha512-fs4Sd8bZLgEzrk8aS7Em1qh+wcawtE87kRUJQhK6+LndyV1HerX7+LURzAylVaTyWIn5NTB/lyjnWqw/AZ6Yrw==
portal-vue@~1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-1.5.1.tgz#6bed79ef168d9676bb79f41d43c5cd4cedf54dbc"
integrity sha512-7T0K+qyY8bnjnEpQTiLbGsUaGlFcemK9gLurVSr6x1/qzr2HkHDNCOz5i+xhuTD1CrXckf/AGeCnLzvmAHMOHw==
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -11686,6 +11691,11 @@ vue-svg-loader@^0.11.0:
loader-utils "^1.1.0"
svg-to-vue "^0.3.0"
vue-sweetalert-icons@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/vue-sweetalert-icons/-/vue-sweetalert-icons-3.2.0.tgz#2926d3af5590b81c0ba3b104212922fc1709396d"
integrity sha512-N18uG8++ZfdCnXO0gHNTmwpB2mAE8WWrwjGeWGa8CnHu6l1emn4RG6E8r1P9crVJ+fx3R9gTUezC+cdVu0mN7w==
vue-template-compiler@^2.5.17:
version "2.5.17"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.17.tgz#52a4a078c327deb937482a509ae85c06f346c3cb"