Merged master in

This commit is contained in:
Grzegorz Leoniec 2019-01-22 15:51:02 +01:00
commit 1718a161c8
No known key found for this signature in database
GPG Key ID: 3AA43686D4EB1377
41 changed files with 1574 additions and 162 deletions

View File

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

View File

@ -125,6 +125,18 @@ hr {
height: 1px !important; height: 1px !important;
} }
[class$=menu-trigger] {
user-select: none;
}
[class$=menu-popover] {
display: inline-block;
nav {
margin-left: -17px;
margin-right: -15px;
}
}
#overlay { #overlay {
display: block; display: block;
opacity: 0; opacity: 0;
@ -142,6 +154,7 @@ hr {
.dropdown-open & { .dropdown-open & {
opacity: 1; opacity: 1;
transition-delay: 0; transition-delay: 0;
transition: opacity 80ms ease-out;
} }
} }
@ -156,6 +169,8 @@ hr {
} }
[class$="menu-popover"] { [class$="menu-popover"] {
min-width: 130px;
a, button { a, button {
display: flex; display: flex;
align-content: center; 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,13 +1,16 @@
<template> <template>
<a
v-router-link
:href="href(post)"
>
<ds-card <ds-card
:header="post.title" :header="post.title"
:image="post.image" :image="post.image"
style="cursor: pointer; position: relative;" class="post-card"
> >
<a
v-router-link
class="post-link"
:href="href(post)"
>
{{ post.title }}
</a>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view --> <!-- TODO: replace editor content with tiptap render view -->
<ds-space margin-bottom="large"> <ds-space margin-bottom="large">
@ -19,7 +22,7 @@
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<ds-space <ds-space
margin="small" margin="small"
style="position: absolute; bottom: 44px;" style="position: absolute; bottom: 44px; z-index: 1;"
> >
<!-- TODO: find better solution for rendering errors --> <!-- TODO: find better solution for rendering errors -->
<no-ssr> <no-ssr>
@ -41,28 +44,34 @@
</div> </div>
<div style="display: inline-block; float: right"> <div style="display: inline-block; float: right">
<span :style="{ opacity: post.shoutedCount ? 1 : .5 }"> <span :style="{ opacity: post.shoutedCount ? 1 : .5 }">
<ds-icon name="bullhorn" /> <ds-icon name="bullhorn" /> <small>{{ post.shoutedCount }}</small>
<small>{{ post.shoutedCount }}</small>
</span> </span>
&nbsp; &nbsp;
<span :style="{ opacity: post.commentsCount ? 1 : .5 }"> <span :style="{ opacity: post.commentsCount ? 1 : .5 }">
<ds-icon name="comments" /> <ds-icon name="comments" /> <small>{{ post.commentsCount }}</small>
<small>{{ post.commentsCount }}</small>
</span> </span>
<no-ssr>
<content-menu
context="contribution"
:item-id="post.id"
:name="post.title"
/>
</no-ssr>
</div> </div>
</template> </template>
</ds-card> </ds-card>
</a>
</template> </template>
<script> <script>
import HcAuthor from '~/components/Author.vue' import HcAuthor from '~/components/Author.vue'
import ContentMenu from '~/components/ContentMenu'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
export default { export default {
name: 'HcPostCard', name: 'HcPostCard',
components: { components: {
HcAuthor HcAuthor,
ContentMenu
}, },
props: { props: {
post: { post: {
@ -96,3 +105,31 @@ export default {
} }
} }
</script> </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 As a user
I would like to add some about me text and a location I would like to add some about me text and a location
So others can get some info about me and my 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 { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { getLangByName } from '../../support/helpers' import { getLangByName } from '../../support/helpers'
import find from 'lodash/find' import users from '../../fixtures/users.json'
/* global cy */ /* global cy */
const username = 'Peter Lustig'
const openPage = page => {
if (page === 'landing') {
page = ''
}
cy.visit(`/${page}`)
}
Given('I am logged in', () => { 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', () => { 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 log out', cy.logout)
When('I visit the {string} page', page => { When('I visit the {string} page', page => {
openPage(page) cy.openPage(page)
}) })
Given('I am on the {string} page', 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', () => { 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() .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', () => { 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', () => { Then('I see the login screen again', () => {
cy.location('pathname').should('contain', '/login') 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', () => { Then('I am still logged in', () => {
cy.get('.avatar-menu').click() 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 => { When('I select {string} in the language menu', name => {
@ -93,3 +88,18 @@ When(`I click on {string}`, linkOrButton => {
When('I press {string}', label => { When('I press {string}', label => {
cy.contains(label).click() 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 */ /* globals Cypress cy */
import { getLangByName } from './helpers' import { getLangByName } from './helpers'
import users from '../fixtures/users.json'
const switchLang = name => { const switchLang = name => {
cy.get('.locale-menu').click() cy.get('.locale-menu').click()
@ -54,11 +55,24 @@ Cypress.Commands.add('login', (email, password) => {
.click() .click()
cy.location('pathname').should('eq', '/') // we're in! 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) => { Cypress.Commands.add('logout', (email, password) => {
cy.visit(`/logout`) cy.visit(`/logout`)
cy.location('pathname').should('contain', '/login') // we're out 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 -- // -- 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' import find from 'lodash/find'
const helpers = { const helpers = {

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

View File

@ -92,6 +92,31 @@
"name": "Einstellungen" "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": { "post": {
"name": "Beitrag", "name": "Beitrag",
"moreInfo": { "moreInfo": {
@ -101,6 +126,25 @@
"name": "Aktiv werden" "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": { "quotes": {
"african": { "african": {
"quote": "Viele kleine Leute, an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.", "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": { "common": {
"reportContent": "Melden",
"post": "Beitrag ::: Beiträge", "post": "Beitrag ::: Beiträge",
"comment": "Kommentar ::: Kommentare", "comment": "Kommentar ::: Kommentare",
"letsTalk": "Miteinander reden", "letsTalk": "Miteinander reden",

View File

@ -92,6 +92,31 @@
"name": "Settings" "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": { "post": {
"name": "Post", "name": "Post",
"moreInfo": { "moreInfo": {
@ -101,6 +126,25 @@
"name": "Take action" "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": { "quotes": {
"african": { "african": {
"quote": "Many small people in many small places do many small things, that can alter the face of the world.", "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": { "common": {
"reportContent": "Report",
"post": "Post ::: Posts", "post": "Post ::: Posts",
"comment": "Comment ::: Comments", "comment": "Comment ::: Comments",
"letsTalk": "Let`s Talk", "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: { router: {
middleware: ['authenticated'], middleware: ['authenticated'],
linkActiveClass: 'router-link-active', 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', 'cookie-universal-nuxt',
'@nuxtjs/apollo', '@nuxtjs/apollo',
'@nuxtjs/axios', '@nuxtjs/axios',
'portal-vue/nuxt',
[ [
'nuxt-sass-resources-loader', 'nuxt-sass-resources-loader',
path.resolve(__dirname, './styleguide/src/system/styles/shared.scss') path.resolve(__dirname, './styleguide/src/system/styles/shared.scss')

View File

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

View File

@ -4,13 +4,13 @@
{{ $t('admin.name') }} {{ $t('admin.name') }}
</ds-heading> </ds-heading>
<ds-flex gutter="small"> <ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }"> <ds-flex-item :width="{ base: '100%', md: '200px' }">
<ds-menu <ds-menu
:routes="routes" :routes="routes"
:is-exact="() => true" :is-exact="() => true"
/> />
</ds-flex-item> </ds-flex-item>
<ds-flex-item> <ds-flex-item :width="{ base: '100%', md: 1 }">
<transition <transition
name="slide-up" name="slide-up"
appear appear
@ -24,6 +24,7 @@
<script> <script>
export default { export default {
middleware: ['isAdmin'],
computed: { computed: {
routes() { routes() {
return [ 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" class="post-card"
> >
<hc-author :post="post" /> <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" /> <ds-space margin-bottom="small" />
<!-- Content --> <!-- Content -->
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
@ -82,6 +90,15 @@
<ds-space margin-bottom="x-small"> <ds-space margin-bottom="x-small">
<hc-author :post="comment" /> <hc-author :post="comment" />
</ds-space> </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 --> <!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view --> <!-- TODO: replace editor content with tiptap render view -->
<div <div
@ -110,6 +127,7 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import ContentMenu from '~/components/ContentMenu'
import HcAuthor from '~/components/Author.vue' import HcAuthor from '~/components/Author.vue'
import HcShoutButton from '~/components/ShoutButton.vue' import HcShoutButton from '~/components/ShoutButton.vue'
import HcEmpty from '~/components/Empty.vue' import HcEmpty from '~/components/Empty.vue'
@ -122,7 +140,8 @@ export default {
components: { components: {
HcAuthor, HcAuthor,
HcShoutButton, HcShoutButton,
HcEmpty HcEmpty,
ContentMenu
}, },
head() { head() {
return { return {
@ -221,7 +240,14 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.post-card { .page-name-post-slug {
.content-menu {
float: right;
margin-right: -$space-x-small;
margin-top: -$space-large;
}
.post-card {
// max-width: 800px; // max-width: 800px;
margin: auto; margin: auto;
@ -241,8 +267,14 @@ export default {
object-position: center; object-position: center;
} }
} }
.ds-card-footer { .ds-card-footer {
padding-bottom: 0; padding: 0;
.ds-section {
padding: $space-base;
}
}
} }
} }
</style> </style>

View File

@ -17,6 +17,14 @@
class="profile-avatar" class="profile-avatar"
size="120px" 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-space margin="small">
<ds-heading <ds-heading
tag="h3" tag="h3"
@ -284,6 +292,7 @@ import HcCountTo from '~/components/CountTo.vue'
import HcBadges from '~/components/Badges.vue' import HcBadges from '~/components/Badges.vue'
import HcLoadMore from '~/components/LoadMore.vue' import HcLoadMore from '~/components/LoadMore.vue'
import HcEmpty from '~/components/Empty.vue' import HcEmpty from '~/components/Empty.vue'
import ContentMenu from '~/components/ContentMenu'
export default { export default {
components: { components: {
@ -293,7 +302,8 @@ export default {
HcCountTo, HcCountTo,
HcBadges, HcBadges,
HcLoadMore, HcLoadMore,
HcEmpty HcEmpty,
ContentMenu
}, },
transition: { transition: {
name: 'slide-up', name: 'slide-up',
@ -402,6 +412,14 @@ export default {
border: #fff 5px solid; 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 { .profile-top-navigation {
position: sticky; position: sticky;
top: 53px; top: 53px;
@ -416,10 +434,10 @@ export default {
&.ds-tab-nav-item-active { &.ds-tab-nav-item-active {
border-bottom: 3px solid #17b53f; border-bottom: 3px solid #17b53f;
&:first-child { &:first-child {
border-bottom-left-radius: $border-radius-large; border-bottom-left-radius: $border-radius-x-large;
} }
&:last-child { &: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') }} {{ $t('settings.name') }}
</ds-heading> </ds-heading>
<ds-flex gutter="small"> <ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }"> <ds-flex-item :width="{ base: '100%', md: '200px' }">
<ds-menu <ds-menu
:routes="routes" :routes="routes"
:is-exact="() => true" :is-exact="() => true"
/> />
</ds-flex-item> </ds-flex-item>
<ds-flex-item> <ds-flex-item :width="{ base: '100%', md: 1 }">
<transition <transition
name="slide-up" name="slide-up"
appear 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" "test:unit": "vue-cli-service test:unit"
}, },
"dependencies": { "dependencies": {
"portal-vue": "^1.5.1",
"vue": "^2.5.17" "vue": "^2.5.17"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,4 +1,4 @@
$border-radius: $border-radius-large; $border-radius: $border-radius-x-large;
.ds-card { .ds-card {
@include reset; @include reset;
@ -7,7 +7,7 @@ $border-radius: $border-radius-large;
flex-direction: column; flex-direction: column;
background-color: $background-color-base; background-color: $background-color-base;
color: $text-color-base; color: $text-color-base;
box-shadow: $box-shadow-large; box-shadow: $box-shadow-base;
height: 100%; 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/background";
@import "./shared/spacing"; @import "./shared/spacing";
@import "./shared/form"; @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: props:
- name: box-shadow-x-large
value: "0 40px 120px 0 rgba(0, 0, 0, .8)"
- name: box-shadow-large - name: box-shadow-large
value: "0 20px 60px 0 rgba(0, 0, 0, .8)" value: "0 20px 60px 0 rgba(0, 0, 0, .8)"
- name: box-shadow-base - name: box-shadow-base

View File

@ -5,11 +5,11 @@
props: props:
- name: border-radius-x-large - name: border-radius-x-large
value: "8px" value: "5px"
- name: border-radius-large - name: border-radius-large
value: "6px" value: "4px"
- name: border-radius-base - name: border-radius-base
value: "3px" value: "4px"
- name: border-radius-rounded - name: border-radius-rounded
value: "2em" value: "2em"
- name: border-radius-circle - 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" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== 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: portfinder@^1.0.19, portfinder@^1.0.9:
version "1.0.19" version "1.0.19"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.19.tgz#07e87914a55242dcda5b833d42f018d6875b595f" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.19.tgz#07e87914a55242dcda5b833d42f018d6875b595f"

View File

@ -8785,6 +8785,11 @@ popper.js@^1.12.9:
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.5.tgz#98abcce7c7c34c4ee47fcbc6b3da8af2c0a127bc" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.5.tgz#98abcce7c7c34c4ee47fcbc6b3da8af2c0a127bc"
integrity sha512-fs4Sd8bZLgEzrk8aS7Em1qh+wcawtE87kRUJQhK6+LndyV1HerX7+LURzAylVaTyWIn5NTB/lyjnWqw/AZ6Yrw== 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: posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -11880,6 +11885,11 @@ vue-svg-loader@^0.11.0:
loader-utils "^1.1.0" loader-utils "^1.1.0"
svg-to-vue "^0.3.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: vue-template-compiler@^2.5.17:
version "2.5.17" version "2.5.17"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.17.tgz#52a4a078c327deb937482a509ae85c06f346c3cb" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.17.tgz#52a4a078c327deb937482a509ae85c06f346c3cb"