diff --git a/.travis.yml b/.travis.yml index 049f097ca..d17bd7cb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..19f3854c1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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/ 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/docker-compose.yml b/docker-compose.yml index 5a4c2ab5d..64b743698 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: 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 aad01265d..7c9649725 100644 --- a/package.json +++ b/package.json @@ -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": { 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 b84683c51..1b4ab6fef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"