diff --git a/assets/styles/imports/_tooltip.scss b/assets/styles/imports/_tooltip.scss index 738d6a158..e3032cb56 100644 --- a/assets/styles/imports/_tooltip.scss +++ b/assets/styles/imports/_tooltip.scss @@ -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; } } diff --git a/assets/styles/main.scss b/assets/styles/main.scss index 61cc21c48..d22f3ec08 100644 --- a/assets/styles/main.scss +++ b/assets/styles/main.scss @@ -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; diff --git a/components/ContentMenu.vue b/components/ContentMenu.vue new file mode 100644 index 000000000..9ab85cb12 --- /dev/null +++ b/components/ContentMenu.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/components/PostCard.vue b/components/PostCard.vue index 6d6f56256..25b410257 100644 --- a/components/PostCard.vue +++ b/components/PostCard.vue @@ -1,68 +1,77 @@ + + diff --git a/components/ReportModal.vue b/components/ReportModal.vue new file mode 100644 index 000000000..1ae23fd94 --- /dev/null +++ b/components/ReportModal.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/cypress/fixtures/users.json b/cypress/fixtures/users.json new file mode 100644 index 000000000..339866774 --- /dev/null +++ b/cypress/fixtures/users.json @@ -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" + } +} diff --git a/cypress/integration/04.AboutMeAndLocation.feature b/cypress/integration/04.AboutMeAndLocation.feature index 83e7046f5..8601f7c80 100644 --- a/cypress/integration/04.AboutMeAndLocation.feature +++ b/cypress/integration/04.AboutMeAndLocation.feature @@ -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 diff --git a/cypress/integration/05.ReportContent.feature b/cypress/integration/05.ReportContent.feature new file mode 100644 index 000000000..fbfdfe8de --- /dev/null +++ b/cypress/integration/05.ReportContent.feature @@ -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 + 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 diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js new file mode 100644 index 000000000..3c546f0f5 --- /dev/null +++ b/cypress/integration/common/report.js @@ -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') +}) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index ee9a364f7..cd4280578 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -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() +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 77c75c7d5..ebbd6acd1 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -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 -- diff --git a/cypress/support/config.js b/cypress/support/config.js deleted file mode 100644 index af96ad615..000000000 --- a/cypress/support/config.js +++ /dev/null @@ -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 - } - } -} diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js index 661682139..1aed57de1 100644 --- a/cypress/support/helpers.js +++ b/cypress/support/helpers.js @@ -1,4 +1,3 @@ - import find from 'lodash/find' const helpers = { diff --git a/graphql/ModerationListQuery.js b/graphql/ModerationListQuery.js new file mode 100644 index 000000000..6126521cc --- /dev/null +++ b/graphql/ModerationListQuery.js @@ -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 + } + } + } + } + `) +} diff --git a/layouts/default.vue b/layouts/default.vue index a511dee8a..7283bf308 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -92,6 +92,12 @@
+ + + + + +
@@ -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; diff --git a/locales/de.json b/locales/de.json index 196360c91..b5d5be9c5 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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 \"{name}\" deaktivieren möchtest?" + }, + "contribution": { + "title": "Beitrag sperren", + "type": "Beitrag", + "message": "Bist du sicher, dass du den Beitrag \"{name}\" deaktivieren möchtest?" + }, + "comment": { + "title": "Kommentar sperren", + "type": "Kommentar", + "message": "Bist du sicher, dass du den Kommentar \"{name}\" 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 \"{name}\" melden möchtest?" + }, + "contribution": { + "title": "Beitrag melden", + "type": "Beitrag", + "message": "Bist du sicher, dass du den Beitrag \"{name}\" melden möchtest?" + }, + "comment": { + "title": "Kommentar melden", + "type": "Kommentar", + "message": "Bist du sicher, dass du den Kommentar von \"{name}\" 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", diff --git a/locales/en.json b/locales/en.json index ab0ce7c1d..023e34835 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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 \"{name}\"?" + }, + "contribution": { + "title": "Disable Contribution", + "type": "Contribution", + "message": "Do you really want to disable the contribution \"{name}\"?" + }, + "comment": { + "title": "Disable Comment", + "type": "Comment", + "message": "Do you really want to disable the comment from \"{name}\"?" + } + }, "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 \"{name}\"?" + }, + "contribution": { + "title": "Report Contribution", + "type": "Contribution", + "message": "Do you really want to report the contribution \"{name}\"?" + }, + "comment": { + "title": "Report Comment", + "type": "Comment", + "message": "Do you really want to report the comment from \"{name}\"?" + } + }, "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", diff --git a/middleware/isAdmin.js b/middleware/isAdmin.js new file mode 100644 index 000000000..4db10bbb6 --- /dev/null +++ b/middleware/isAdmin.js @@ -0,0 +1,5 @@ +export default ({ store, error }) => { + if (!store.getters['auth/isAdmin']) { + return error({ statusCode: 403 }) + } +} diff --git a/middleware/isModerator.js b/middleware/isModerator.js new file mode 100644 index 000000000..e99793a3e --- /dev/null +++ b/middleware/isModerator.js @@ -0,0 +1,5 @@ +export default ({ store, error }) => { + if (!store.getters['auth/isModerator']) { + return error({ statusCode: 403 }) + } +} diff --git a/nuxt.config.js b/nuxt.config.js index a1f3e824b..b8cc2d50c 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -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') diff --git a/package.json b/package.json index 940f8cae3..33464ccc3 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,11 @@ "nuxt-sass-resources-loader": "^2.0.5", "tiptap": "~1.8.0", "tiptap-extensions": "~1.8.0", + "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": { diff --git a/pages/admin.vue b/pages/admin.vue index b7a3049ba..7149109b1 100644 --- a/pages/admin.vue +++ b/pages/admin.vue @@ -4,13 +4,13 @@ {{ $t('admin.name') }} - + - + export default { + middleware: ['isAdmin'], computed: { routes() { return [ diff --git a/pages/moderation.vue b/pages/moderation.vue new file mode 100644 index 000000000..6b151b0e4 --- /dev/null +++ b/pages/moderation.vue @@ -0,0 +1,36 @@ + + + diff --git a/pages/moderation/index.vue b/pages/moderation/index.vue new file mode 100644 index 000000000..995c9d560 --- /dev/null +++ b/pages/moderation/index.vue @@ -0,0 +1,116 @@ + + + diff --git a/pages/post/_slug/index.vue b/pages/post/_slug/index.vue index 73a9a7c7b..200a9f472 100644 --- a/pages/post/_slug/index.vue +++ b/pages/post/_slug/index.vue @@ -6,6 +6,14 @@ class="post-card" > + + + @@ -82,6 +90,15 @@ + + +
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 { diff --git a/pages/profile/_slug.vue b/pages/profile/_slug.vue index c8b7139c2..ad99402c7 100644 --- a/pages/profile/_slug.vue +++ b/pages/profile/_slug.vue @@ -17,6 +17,14 @@ class="profile-avatar" size="120px" /> + + + - + - + { + 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 + } +} diff --git a/styleguide/package.json b/styleguide/package.json index 360bfaf83..58a232f54 100644 --- a/styleguide/package.json +++ b/styleguide/package.json @@ -14,6 +14,7 @@ "test:unit": "vue-cli-service test:unit" }, "dependencies": { + "portal-vue": "^1.5.1", "vue": "^2.5.17" }, "devDependencies": { diff --git a/styleguide/src/system/components/layout/Card/style.scss b/styleguide/src/system/components/layout/Card/style.scss index 2ce09bff2..742aa2b00 100644 --- a/styleguide/src/system/components/layout/Card/style.scss +++ b/styleguide/src/system/components/layout/Card/style.scss @@ -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%; } diff --git a/styleguide/src/system/components/layout/Modal/Modal.vue b/styleguide/src/system/components/layout/Modal/Modal.vue new file mode 100644 index 000000000..4a273e680 --- /dev/null +++ b/styleguide/src/system/components/layout/Modal/Modal.vue @@ -0,0 +1,185 @@ + + + + + + + diff --git a/styleguide/src/system/components/layout/Modal/demo.md b/styleguide/src/system/components/layout/Modal/demo.md new file mode 100644 index 000000000..50f9481e2 --- /dev/null +++ b/styleguide/src/system/components/layout/Modal/demo.md @@ -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 + + + +``` + +``` + + +``` + +Customize button labels +``` + + +``` diff --git a/styleguide/src/system/components/layout/Modal/style.scss b/styleguide/src/system/components/layout/Modal/style.scss new file mode 100644 index 000000000..8b4e9f4a5 --- /dev/null +++ b/styleguide/src/system/components/layout/Modal/style.scss @@ -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); +} diff --git a/styleguide/src/system/icons/svg/flag.svg b/styleguide/src/system/icons/svg/flag.svg new file mode 100755 index 000000000..6d2769808 --- /dev/null +++ b/styleguide/src/system/icons/svg/flag.svg @@ -0,0 +1,5 @@ + + +flag + + diff --git a/styleguide/src/system/styles/shared.scss b/styleguide/src/system/styles/shared.scss index e2da9cb8e..187740b1a 100644 --- a/styleguide/src/system/styles/shared.scss +++ b/styleguide/src/system/styles/shared.scss @@ -16,3 +16,5 @@ @import "./shared/background"; @import "./shared/spacing"; @import "./shared/form"; +@import "./shared/transitions"; +@import "./shared/animations"; diff --git a/styleguide/src/system/styles/shared/_animations.scss b/styleguide/src/system/styles/shared/_animations.scss new file mode 100644 index 000000000..c1987dcf5 --- /dev/null +++ b/styleguide/src/system/styles/shared/_animations.scss @@ -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; +} diff --git a/styleguide/src/system/styles/shared/_transitions.scss b/styleguide/src/system/styles/shared/_transitions.scss new file mode 100644 index 000000000..805f031dd --- /dev/null +++ b/styleguide/src/system/styles/shared/_transitions.scss @@ -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; +} diff --git a/styleguide/src/system/tokens/_examples/dark-purple/box-shadow.yml b/styleguide/src/system/tokens/_examples/dark-purple/box-shadow.yml index 59fb1e83d..de107d0ac 100644 --- a/styleguide/src/system/tokens/_examples/dark-purple/box-shadow.yml +++ b/styleguide/src/system/tokens/_examples/dark-purple/box-shadow.yml @@ -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 diff --git a/styleguide/src/system/tokens/border-radius.yml b/styleguide/src/system/tokens/border-radius.yml index 237f3944b..f299da252 100644 --- a/styleguide/src/system/tokens/border-radius.yml +++ b/styleguide/src/system/tokens/border-radius.yml @@ -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 diff --git a/styleguide/yarn.lock b/styleguide/yarn.lock index c2f3906e8..23dad73dd 100644 --- a/styleguide/yarn.lock +++ b/styleguide/yarn.lock @@ -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" diff --git a/yarn.lock b/yarn.lock index ea6a715b0..73af7da69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8785,6 +8785,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" @@ -11880,6 +11885,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"