├── .babelrc ├── .dockerignore ├── .editorconfig ├── .env.template ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── assets ├── README.md └── styles │ ├── imports │ ├── _toast.scss │ └── _tooltip.scss │ └── main.scss ├── components ├── Badges.spec.js ├── Badges.vue ├── Comment.spec.js ├── Comment.vue ├── ContentMenu.vue ├── ContributionForm.vue ├── CountTo.vue ├── Dropdown.vue ├── Editor │ ├── Editor.vue │ └── plugins │ │ └── eventHandler.js ├── Empty.vue ├── FollowButton.vue ├── LoadMore.vue ├── LocaleSwitch.vue ├── Logo.vue ├── Modal.spec.js ├── Modal.vue ├── Modal │ ├── DisableModal.spec.js │ ├── DisableModal.vue │ ├── ReportModal.spec.js │ └── ReportModal.vue ├── PostCard.vue ├── README.md ├── SearchInput.spec.js ├── SearchInput.vue ├── ShoutButton.vue ├── User.spec.js ├── User.vue └── mixins │ └── seo.js ├── cypress.env.template.json ├── cypress.json ├── cypress ├── fixtures │ ├── example.json │ └── users.json ├── integration │ ├── 01.Login.feature │ ├── 02.Internationalization.feature │ ├── 03.TagsAndCategories.feature │ ├── 04.AboutMeAndLocation.feature │ ├── 06.Search.feature │ ├── 06.WritePost.feature │ ├── common │ │ ├── admin.js │ │ ├── report.js │ │ ├── search.js │ │ ├── settings.js │ │ └── steps.js │ └── moderation │ │ ├── HidePosts.feature │ │ └── ReportContent.feature ├── plugins │ └── index.js └── support │ ├── commands.js │ ├── factories.js │ ├── helpers.js │ └── index.js ├── docker-compose.override.yml ├── docker-compose.travis.yml ├── docker-compose.yml ├── graphql ├── ModerationListQuery.js ├── PostMutations.js └── UserProfileQuery.js ├── layouts ├── README.md ├── blank.vue └── default.vue ├── locales ├── de.json ├── en.json ├── es.json ├── fr.json ├── index.js ├── it.json ├── nl.json ├── pl.json └── pt.json ├── lokalise.png ├── middleware ├── README.md ├── authenticated.js ├── isAdmin.js └── isModerator.js ├── nuxt.config.js ├── package.json ├── pages ├── README.md ├── admin.vue ├── admin │ ├── categories.vue │ ├── index.vue │ ├── notifications.vue │ ├── organizations.vue │ ├── pages.vue │ ├── settings.vue │ ├── tags.vue │ └── users.vue ├── index.vue ├── login.vue ├── logout.vue ├── moderation.vue ├── moderation │ └── index.vue ├── post │ ├── _slug.vue │ ├── _slug │ │ ├── index.vue │ │ ├── more-info.vue │ │ └── take-action.vue │ ├── create.vue │ └── edit │ │ └── _id.vue ├── profile │ ├── _slug.vue │ └── index.vue ├── settings.vue └── settings │ ├── data-download.vue │ ├── delete-account.vue │ ├── index.vue │ ├── invites.vue │ ├── languages.vue │ ├── my-organizations.vue │ └── security.vue ├── plugins ├── README.md ├── apollo-config.js ├── axios.js ├── i18n.js ├── izi-toast.js ├── keep-alive.js ├── styleguide-dev.js ├── styleguide.js ├── v-tooltip.js ├── vue-directives.js └── vue-filters.js ├── screenshot-styleguide.png ├── screenshot.png ├── scripts ├── deploy.sh └── docker_push.sh ├── server └── index.js ├── static ├── README.md ├── favicon.ico └── img │ ├── badges │ ├── fundraisingbox_de_airship.svg │ ├── fundraisingbox_de_alienship.svg │ ├── fundraisingbox_de_balloon.svg │ ├── fundraisingbox_de_bigballoon.svg │ ├── fundraisingbox_de_crane.svg │ ├── fundraisingbox_de_glider.svg │ ├── fundraisingbox_de_helicopter.svg │ ├── fundraisingbox_de_starter.svg │ ├── indiegogo_en_bear.svg │ ├── indiegogo_en_panda.svg │ ├── indiegogo_en_rabbit.svg │ ├── indiegogo_en_racoon.svg │ ├── indiegogo_en_rhino.svg │ ├── indiegogo_en_tiger.svg │ ├── indiegogo_en_turtle.svg │ ├── indiegogo_en_whale.svg │ ├── indiegogo_en_wolf.svg │ ├── user_role_admin.svg │ ├── user_role_developer.svg │ ├── user_role_moderator.svg │ ├── wooold_de_bee.svg │ ├── wooold_de_butterfly.svg │ ├── wooold_de_double_rainbow.svg │ ├── wooold_de_end_of_rainbow.svg │ ├── wooold_de_flower.svg │ ├── wooold_de_lifetree.svg │ ├── wooold_de_magic_rainbow.svg │ └── wooold_de_super_founder.svg │ ├── empty-state.svg │ ├── empty │ ├── alert.svg │ ├── docs.svg │ ├── events.svg │ ├── file.svg │ ├── messages.svg │ └── tasks.svg │ ├── locale-flags │ ├── de.svg │ ├── en.svg │ ├── es.svg │ ├── fr.svg │ ├── it.svg │ ├── nl.svg │ ├── pl.svg │ └── pt.svg │ └── sign-up │ ├── alpha-invite.png │ ├── alpha-invite2x.png │ ├── humanconnection.png │ ├── humanconnection.svg │ ├── nicetomeetyou.png │ ├── nicetomeetyou.svg │ ├── onourjourney.png │ └── onourjourney.svg ├── store ├── README.md ├── auth.js ├── auth.test.js ├── index.js ├── modal.js └── search.js ├── styleguide └── src │ └── system │ └── icons │ └── svg │ ├── bold.svg │ ├── italic.svg │ ├── list-ol.svg │ ├── list-ul.svg │ ├── paragraph.svg │ └── quote-right.svg └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "env": { 11 | "test": { 12 | "presets": [ 13 | [ 14 | "@babel/preset-env", 15 | { 16 | "targets": { 17 | "node": "10" 18 | } 19 | } 20 | ] 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | styleguide/ 4 | node_modules/ 5 | npm-debug.log 6 | 7 | Dockerfile 8 | docker-compose*.yml 9 | scripts/ 10 | 11 | .env 12 | 13 | cypress/ 14 | 15 | README.md 16 | screenshot*.png 17 | lokalise.png 18 | .editorconfig 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .nuxt 4 | styleguide/ 5 | **/*.min.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | 'plugin:vue/recommended', 12 | 'plugin:prettier/recommended' 13 | ], 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'vue', 17 | 'prettier' 18 | ], 19 | // add your custom rules here 20 | rules: { 21 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 23 | 'vue/component-name-in-template-casing': ['error', 'kebab-case'] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to 'http...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (https://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules/ 33 | styleguide/ 34 | 35 | # TypeScript v1 declaration files 36 | typings/ 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | # dotenv environment variables file 54 | .env 55 | 56 | # parcel-bundler cache (https://parceljs.org/) 57 | .cache 58 | 59 | # next.js build output 60 | .next 61 | 62 | # nuxt.js build output 63 | .nuxt 64 | 65 | # Nuxt generate 66 | dist 67 | 68 | #ignore internal github files 69 | /.github 70 | 71 | # Serverless directories 72 | .serverless 73 | 74 | # IDE 75 | .idea 76 | .vscode 77 | 78 | # TEMORIRY 79 | static/uploads 80 | 81 | cypress/videos 82 | cypress/screenshots/ 83 | cypress.env.json 84 | 85 | # Apple macOS folder attribute file 86 | .DS_Store 87 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "bracketSpacing": true 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | cache: 5 | yarn: true 6 | directories: 7 | - node_modules 8 | services: 9 | - docker 10 | addons: 11 | chrome: stable 12 | apt: 13 | sources: 14 | - google-chrome 15 | packages: 16 | - google-chrome-stable 17 | 18 | env: 19 | - DOCKER_COMPOSE_VERSION=1.23.2 BACKEND_BRANCH=${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH:-master}} 20 | 21 | 22 | before_install: 23 | - echo $BACKEND_BRANCH 24 | - sudo rm /usr/local/bin/docker-compose 25 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 26 | - chmod +x docker-compose 27 | - sudo mv docker-compose /usr/local/bin 28 | - cp cypress.env.template.json cypress.env.json 29 | 30 | install: 31 | - docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-web . 32 | - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d 33 | - git clone https://github.com/Human-Connection/Nitro-Backend.git ../Nitro-Backend 34 | - git -C "../Nitro-Backend" checkout $BACKEND_BRANCH || git -C "../Nitro-Backend" checkout master 35 | - cd ../Nitro-Backend && yarn install && cd - 36 | - docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.cypress.yml up -d 37 | - yarn global add cypress wait-on 38 | - yarn add cypress-cucumber-preprocessor 39 | 40 | script: 41 | - docker-compose exec -e NODE_ENV=test webapp yarn run lint 42 | - docker-compose exec -e NODE_ENV=test webapp yarn run test 43 | - wait-on http://localhost:7474 && docker-compose -f ../Nitro-Backend/docker-compose.yml exec neo4j migrate 44 | - wait-on http://localhost:3000 && cypress run --browser chrome --record --key $CYPRESS_TOKEN 45 | 46 | after_success: 47 | - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh 48 | - chmod +x send.sh 49 | - ./send.sh success $WEBHOOK_URL 50 | - if [ $TRAVIS_BRANCH == "master" ] && [ $TRAVIS_EVENT_TYPE == "push" ]; then 51 | wget https://raw.githubusercontent.com/Human-Connection/Discord-Bot/develop/tester.sh && 52 | chmod +x tester.sh && 53 | ./tester.sh staging $WEBHOOK_URL; 54 | fi 55 | 56 | after_failure: 57 | - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh 58 | - chmod +x send.sh 59 | - ./send.sh failure $WEBHOOK_URL 60 | 61 | deploy: 62 | - provider: script 63 | script: scripts/docker_push.sh 64 | on: 65 | branch: master 66 | - provider: script 67 | script: scripts/deploy.sh nitro.human-connection.org 68 | on: 69 | branch: master 70 | tags: true 71 | - provider: script 72 | script: scripts/deploy.sh nitro-staging.human-connection.org 73 | on: 74 | branch: master 75 | - provider: script 76 | script: scripts/deploy.sh "nitro-$(git rev-parse --short HEAD).human-connection.org" 77 | on: 78 | tags: true 79 | all_branches: true 80 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | 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. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | 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. 28 | 29 | 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. 30 | 31 | ## Scope 32 | 33 | 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. 34 | 35 | ## Enforcement 36 | 37 | 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. 38 | 39 | 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. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine as base 2 | LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" 3 | 4 | EXPOSE 3000 5 | CMD ["yarn", "run", "start"] 6 | 7 | # Expose the app port 8 | ARG BUILD_COMMIT 9 | ENV BUILD_COMMIT=$BUILD_COMMIT 10 | ARG WORKDIR=/nitro-web 11 | RUN mkdir -p $WORKDIR 12 | WORKDIR $WORKDIR 13 | 14 | # See: https://github.com/nodejs/docker-node/pull/367#issuecomment-430807898 15 | RUN apk --no-cache add git 16 | 17 | COPY . . 18 | 19 | FROM base as build-and-test 20 | RUN cp .env.template .env 21 | RUN yarn install --production=false --frozen-lockfile --non-interactive 22 | RUN yarn run build 23 | 24 | FROM base as production 25 | ENV NODE_ENV=production 26 | COPY --from=build-and-test ./nitro-web/node_modules ./node_modules 27 | COPY --from=build-and-test ./nitro-web/.nuxt ./.nuxt 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Human-Connection gGmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Human Connection 3 |

4 | 5 | # NITRO Web 6 | [![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Web/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Web) 7 | [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Web/blob/master/LICENSE.md) 8 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web?ref=badge_shield) 9 | [![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3) 10 | 11 | ![UI Screenshot](screenshot.png) 12 | 13 | ## Build Setup 14 | 15 | 16 | 17 | ### Install 18 | ``` bash 19 | # install all dependencies 20 | $ yarn install 21 | ``` 22 | 23 | Copy: 24 | ``` 25 | cp .env.template .env 26 | cp cypress.env.template.json cypress.env.json 27 | ``` 28 | Configure the files according to your needs and your local setup. 29 | 30 | ### Development 31 | ``` bash 32 | # serve with hot reload at localhost:3000 33 | $ yarn dev 34 | ``` 35 | 36 | ### Build for production 37 | ``` bash 38 | # build for production and launch server 39 | $ yarn build 40 | $ yarn start 41 | ``` 42 | 43 | ## Styleguide 44 | 45 | All reusable Components (for example avatar) should be done inside the [Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide) repository. 46 | 47 | ![Styleguide Screenshot](screenshot-styleguide.png) 48 | 49 | More information can be found here: https://github.com/Human-Connection/Nitro-Styleguide 50 | 51 | 52 | If you need to change something in the styleguide and want to see the effects on the frontend immediately, then we have you covered. 53 | You need to clone the styleguide to the parent directory `../Nitro-Styleguide` and run `yarn && yarn run dev`. After that you run `yarn run dev:styleguide` instead of `yarn run dev` and you will see your changes reflected inside the fronten! 54 | 55 | ## Internationalization (i18n) 56 | 57 | You can help translating the interface by joining us on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/). 58 | 59 | Thanks lokalise.co that we can use your premium account! 60 | 61 | localise.co 62 | 63 | ## Attributions 64 | 65 |
Locale Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
66 | 67 | ## License 68 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web?ref=badge_large) 69 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /assets/styles/imports/_toast.scss: -------------------------------------------------------------------------------- 1 | .iziToast-target, .iziToast { 2 | &, 3 | &:after, 4 | &.iziToast-color-dark:after { 5 | box-shadow: none !important; 6 | } 7 | } 8 | 9 | .iziToast .iziToast-message { 10 | font-weight: 400 !important; 11 | } 12 | 13 | .iziToast.iziToast-color-red { 14 | background: $color-danger !important; 15 | border-color: $color-danger !important; 16 | } 17 | .iziToast.iziToast-color-orange { 18 | background: $color-warning !important; 19 | border-color: $color-warning !important; 20 | } 21 | .iziToast.iziToast-color-yellow { 22 | background: $color-yellow !important; 23 | border-color: $color-yellow !important; 24 | } 25 | .iziToast.iziToast-color-blue { 26 | background: $color-secondary !important; 27 | border-color: $color-secondary !important; 28 | } 29 | .iziToast.iziToast-color-green { 30 | background: $color-success !important; 31 | border-color: $color-success !important; 32 | } 33 | -------------------------------------------------------------------------------- /assets/styles/imports/_tooltip.scss: -------------------------------------------------------------------------------- 1 | @mixin arrow($size, $type, $color) { 2 | 3 | --#{$type}-arrow-size: $size; 4 | 5 | .#{$type}-arrow { 6 | width: 0; 7 | height: 0; 8 | border-style: solid; 9 | position: absolute; 10 | margin: $size; 11 | border-color: $color; 12 | z-index: 1; 13 | } 14 | 15 | &[x-placement^="top"] { 16 | margin-bottom: $size; 17 | 18 | .#{$type}-arrow { 19 | border-width: $size $size 0 $size; 20 | border-left-color: transparent !important; 21 | border-right-color: transparent !important; 22 | border-bottom-color: transparent !important; 23 | bottom: -$size; 24 | left: calc(50% - var(--#{$type}-arrow-size)); 25 | margin-top: 0; 26 | margin-bottom: 0; 27 | } 28 | } 29 | 30 | &[x-placement^="bottom"] { 31 | margin-top: $size; 32 | 33 | .#{$type}-arrow { 34 | border-width: 0 $size $size $size; 35 | border-left-color: transparent !important; 36 | border-right-color: transparent !important; 37 | border-top-color: transparent !important; 38 | top: -$size; 39 | left: calc(50% - var(--#{$type}-arrow-size)); 40 | margin-top: 0; 41 | margin-bottom: 0; 42 | } 43 | } 44 | 45 | &[x-placement^="right"] { 46 | margin-left: $size; 47 | 48 | .#{$type}-arrow { 49 | border-width: $size $size $size 0; 50 | border-left-color: transparent !important; 51 | border-top-color: transparent !important; 52 | border-bottom-color: transparent !important; 53 | left: -$size; 54 | top: calc(50% - var(--#{$type}-arrow-size)); 55 | margin-left: 0; 56 | margin-right: 0; 57 | } 58 | } 59 | 60 | &[x-placement^="left"] { 61 | margin-right: $size; 62 | 63 | .#{$type}-arrow { 64 | border-width: $size 0 $size $size; 65 | border-top-color: transparent !important; 66 | border-right-color: transparent !important; 67 | border-bottom-color: transparent !important; 68 | right: -$size; 69 | top: calc(50% - var(--#{$type}-arrow-size)); 70 | margin-left: 0; 71 | margin-right: 0; 72 | } 73 | } 74 | } 75 | 76 | .tooltip { 77 | display: block !important; 78 | z-index: $z-index-modal - 2; 79 | 80 | .tooltip-inner { 81 | background: $background-color-inverse-soft; 82 | color: $text-color-inverse; 83 | border-radius: $border-radius-base; 84 | padding: $space-x-small $space-small; 85 | box-shadow: $box-shadow-large; 86 | } 87 | 88 | @include arrow(5px, "tooltip", $background-color-inverse-soft); 89 | 90 | &.popover { 91 | .popover-inner { 92 | background: $background-color-soft; 93 | color: $text-color-base; 94 | border-radius: $border-radius-base; 95 | padding: $space-x-small $space-small; 96 | box-shadow: $box-shadow-x-large; 97 | 98 | nav { 99 | margin-left: -$space-small; 100 | margin-right: -$space-small; 101 | 102 | a { 103 | padding-left: 12px; 104 | } 105 | } 106 | } 107 | 108 | .popover-arrow { 109 | border-color: $background-color-soft; 110 | } 111 | 112 | @include arrow(7px, "popover", $background-color-soft); 113 | } 114 | 115 | 116 | &[aria-hidden='true'] { 117 | visibility: hidden; 118 | opacity: 0; 119 | transition: opacity 60ms; 120 | } 121 | 122 | &[aria-hidden='false'] { 123 | visibility: visible; 124 | opacity: 1; 125 | transition: opacity 60ms; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /assets/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import './imports/_tooltip.scss'; 2 | @import './imports/_toast.scss'; 3 | 4 | // Transition Easing 5 | $easeOut: cubic-bezier(0.19, 1, 0.22, 1); 6 | 7 | .disabled-content { 8 | position: relative; 9 | 10 | &::before { 11 | @include border-radius($border-radius-x-large); 12 | box-shadow: inset 0 0 0 5px $color-danger; 13 | content: ""; 14 | display: block; 15 | position: absolute; 16 | width: 100%; 17 | height: 100%; 18 | z-index: 2; 19 | pointer-events: none; 20 | } 21 | } 22 | 23 | .layout-enter-active { 24 | transition: opacity 80ms ease-out; 25 | transition-delay: 80ms; 26 | } 27 | .layout-leave-active { 28 | transition: opacity 80ms ease-in; 29 | } 30 | .layout-enter, 31 | .layout-leave-active { 32 | opacity: 0; 33 | } 34 | 35 | // slide up ease 36 | .slide-up-enter-active { 37 | transition: all 500ms $easeOut; 38 | transition-delay: 20ms; 39 | opacity: 1; 40 | transform: translate3d(0, 0, 0); 41 | } 42 | .slide-up-enter, 43 | .slide-up-leave-active { 44 | opacity: 0; 45 | box-shadow: none; 46 | transform: translate3d(0, 15px, 0); 47 | } 48 | 49 | .main-navigation { 50 | background: #fff; 51 | } 52 | 53 | blockquote { 54 | display: block; 55 | padding: 15px 20px 15px 45px; 56 | margin: 0 0 20px; 57 | position: relative; 58 | 59 | /*Font*/ 60 | font-size: $font-size-base; 61 | line-height: 1.2; 62 | color: $color-neutral-40; 63 | font-family: $font-family-serif; 64 | font-style: italic; 65 | 66 | border-left: 3px dotted $color-neutral-70; 67 | 68 | &::before { 69 | content: '\201C'; /*Unicode for Left Double Quote*/ 70 | 71 | /*Font*/ 72 | font-size: $font-size-xxxx-large; 73 | font-weight: bold; 74 | color: $color-neutral-50; 75 | 76 | /*Positioning*/ 77 | position: absolute; 78 | left: 10px; 79 | top: 5px; 80 | } 81 | 82 | p { 83 | margin-top: 0; 84 | } 85 | } 86 | .main-navigation { 87 | box-shadow: $box-shadow-base; 88 | position: fixed; 89 | width: 100%; 90 | z-index: 10; 91 | 92 | a { 93 | outline: none; 94 | } 95 | } 96 | 97 | hr { 98 | border: 0; 99 | width: 100%; 100 | color: $color-neutral-80; 101 | background-color: $color-neutral-80; 102 | height: 1px !important; 103 | } 104 | 105 | [class$=menu-trigger] { 106 | user-select: none; 107 | } 108 | [class$=menu-popover] { 109 | display: inline-block; 110 | 111 | nav { 112 | margin-left: -17px; 113 | margin-right: -15px; 114 | } 115 | } 116 | 117 | #overlay { 118 | display: block; 119 | opacity: 0; 120 | width: 100%; 121 | height: 100%; 122 | top: 0; 123 | left: 0; 124 | position: fixed; 125 | background: rgba(0, 0, 0, 0.15); 126 | z-index: 99; 127 | pointer-events: none; 128 | transition: opacity 150ms ease-out; 129 | transition-delay: 50ms; 130 | 131 | .dropdown-open & { 132 | opacity: 1; 133 | transition-delay: 0; 134 | transition: opacity 80ms ease-out; 135 | } 136 | } 137 | 138 | .ds-card .ds-section { 139 | padding: 0; 140 | margin-left: -$space-base; 141 | margin-right: -$space-base; 142 | 143 | .ds-container { 144 | padding: $space-base; 145 | } 146 | } 147 | 148 | [class$="menu-popover"] { 149 | min-width: 130px; 150 | 151 | a, button { 152 | display: flex; 153 | align-content: center; 154 | align-items: center; 155 | 156 | .ds-icon { 157 | padding-right: $space-xx-small; 158 | } 159 | } 160 | } 161 | 162 | .v-popover.open .trigger a { 163 | color: $text-color-link-active; 164 | } 165 | -------------------------------------------------------------------------------- /components/Badges.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Badges from './Badges.vue' 3 | 4 | describe('Badges.vue', () => { 5 | let wrapper 6 | 7 | beforeEach(() => { 8 | wrapper = shallowMount(Badges, {}) 9 | }) 10 | 11 | it('renders', () => { 12 | expect(wrapper.is('div')).toBe(true) 13 | }) 14 | 15 | it('has class "hc-badges"', () => { 16 | expect(wrapper.contains('.hc-badges')).toBe(true) 17 | }) 18 | 19 | // TODO: add similar software tests for other components 20 | // TODO: add more test cases in this file 21 | }) 22 | -------------------------------------------------------------------------------- /components/Badges.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 32 | 33 | 82 | -------------------------------------------------------------------------------- /components/Comment.spec.js: -------------------------------------------------------------------------------- 1 | import { config, shallowMount, mount, createLocalVue } from '@vue/test-utils' 2 | import Comment from './Comment.vue' 3 | import Vue from 'vue' 4 | import Vuex from 'vuex' 5 | import Styleguide from '@human-connection/styleguide' 6 | 7 | const localVue = createLocalVue() 8 | 9 | localVue.use(Vuex) 10 | localVue.use(Styleguide) 11 | 12 | config.stubs['no-ssr'] = '' 13 | 14 | describe('Comment.vue', () => { 15 | let wrapper 16 | let Wrapper 17 | let propsData 18 | let mocks 19 | let getters 20 | 21 | beforeEach(() => { 22 | propsData = {} 23 | mocks = { 24 | $t: jest.fn() 25 | } 26 | getters = { 27 | 'auth/user': () => { 28 | return {} 29 | }, 30 | 'auth/isModerator': () => false 31 | } 32 | }) 33 | 34 | describe('shallowMount', () => { 35 | const Wrapper = () => { 36 | const store = new Vuex.Store({ 37 | getters 38 | }) 39 | return shallowMount(Comment, { store, propsData, mocks, localVue }) 40 | } 41 | 42 | describe('given a comment', () => { 43 | beforeEach(() => { 44 | propsData.comment = { 45 | id: '2', 46 | contentExcerpt: 'Hello I am a comment content' 47 | } 48 | }) 49 | 50 | it('renders content', () => { 51 | const wrapper = Wrapper() 52 | expect(wrapper.text()).toMatch('Hello I am a comment content') 53 | }) 54 | 55 | describe('which is disabled', () => { 56 | beforeEach(() => { 57 | propsData.comment.disabled = true 58 | }) 59 | 60 | it('renders no comment data', () => { 61 | const wrapper = Wrapper() 62 | expect(wrapper.text()).not.toMatch('comment content') 63 | }) 64 | 65 | it('has no "disabled-content" css class', () => { 66 | const wrapper = Wrapper() 67 | expect(wrapper.classes()).not.toContain('disabled-content') 68 | }) 69 | 70 | it('translates a placeholder', () => { 71 | const wrapper = Wrapper() 72 | const calls = mocks.$t.mock.calls 73 | const expected = [['comment.content.unavailable-placeholder']] 74 | expect(calls).toEqual(expect.arrayContaining(expected)) 75 | }) 76 | 77 | describe('for a moderator', () => { 78 | beforeEach(() => { 79 | getters['auth/isModerator'] = () => true 80 | }) 81 | 82 | it('renders comment data', () => { 83 | const wrapper = Wrapper() 84 | expect(wrapper.text()).toMatch('comment content') 85 | }) 86 | 87 | it('has a "disabled-content" css class', () => { 88 | const wrapper = Wrapper() 89 | expect(wrapper.classes()).toContain('disabled-content') 90 | }) 91 | }) 92 | }) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /components/Comment.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 77 | -------------------------------------------------------------------------------- /components/CountTo.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 51 | -------------------------------------------------------------------------------- /components/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 135 | -------------------------------------------------------------------------------- /components/Editor/plugins/eventHandler.js: -------------------------------------------------------------------------------- 1 | import { Extension, Plugin } from 'tiptap' 2 | // import { Slice, Fragment } from 'prosemirror-model' 3 | 4 | export default class EventHandler extends Extension { 5 | get name() { 6 | return 'event_handler' 7 | } 8 | get plugins() { 9 | return [ 10 | new Plugin({ 11 | props: { 12 | transformPastedText(text) { 13 | // console.log('#### transformPastedText', text) 14 | return text.trim() 15 | }, 16 | transformPastedHTML(html) { 17 | html = html 18 | // remove all tags with "space only" 19 | .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '') 20 | // remove all iframes 21 | .replace( 22 | /(]*)(>)[^>]*\/*>/gim, 23 | '' 24 | ) 25 | .replace(/[\n]{3,}/gim, '\n\n') 26 | .replace(/(\r\n|\n\r|\r|\n)/g, '
$1') 27 | 28 | // replace all p tags with line breaks (and spaces) only by single linebreaks 29 | // limit linebreaks to max 2 (equivalent to html "br" linebreak) 30 | .replace(/(
\s*){2,}/gim, '
') 31 | // remove additional linebreaks after p tags 32 | .replace( 33 | /<\/(p|div|th|tr)>\s*(
\s*)+\s*<(p|div|th|tr)>/gim, 34 | '

' 35 | ) 36 | // remove additional linebreaks inside p tags 37 | .replace( 38 | /<[a-z-]+>(<[a-z-]+>)*\s*(
\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, 39 | '' 40 | ) 41 | // remove additional linebreaks when first child inside p tags 42 | .replace(/

(\s*
\s*)+/gim, '

') 43 | // remove additional linebreaks when last child inside p tags 44 | .replace(/(\s*
\s*)+<\/p>/gim, '

') 45 | // console.log('#### transformPastedHTML', html) 46 | return html 47 | } 48 | // transformPasted(slice) { 49 | // // console.log('#### transformPasted', slice.content) 50 | // let content = [] 51 | // let size = 0 52 | // slice.content.forEach((node, offset, index) => { 53 | // // console.log(node) 54 | // // console.log('isBlock', node.type.isBlock) 55 | // // console.log('childCount', node.content.childCount) 56 | // // console.log('index', index) 57 | // if (node.content.childCount) { 58 | // content.push(node.content) 59 | // size += node.content.size 60 | // } 61 | // }) 62 | // console.log(content) 63 | // console.log(slice.content) 64 | // let fragment = Fragment.fromArray(content) 65 | // fragment.size = size 66 | // console.log('#fragment', fragment, slice.content) 67 | // console.log('----') 68 | // console.log('#1', slice) 69 | // // const newSlice = new Slice(fragment, slice.openStart, slice.openEnd) 70 | // slice.fragment = fragment 71 | // // slice.content.content = fragment.content 72 | // // slice.content.size = fragment.size 73 | // console.log('#2', slice) 74 | // // console.log(newSlice) 75 | // console.log('----') 76 | // return slice 77 | // // return newSlice 78 | // } 79 | } 80 | }) 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /components/Empty.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /components/FollowButton.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 92 | -------------------------------------------------------------------------------- /components/LoadMore.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /components/LocaleSwitch.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 88 | 89 | 109 | -------------------------------------------------------------------------------- /components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 80 | -------------------------------------------------------------------------------- /components/Modal.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount, createLocalVue } from '@vue/test-utils' 2 | import Modal from './Modal.vue' 3 | import DisableModal from './Modal/DisableModal.vue' 4 | import ReportModal from './Modal/ReportModal.vue' 5 | import Vue from 'vue' 6 | import Vuex from 'vuex' 7 | import { getters, mutations } from '../store/modal' 8 | import Styleguide from '@human-connection/styleguide' 9 | 10 | const localVue = createLocalVue() 11 | 12 | localVue.use(Vuex) 13 | localVue.use(Styleguide) 14 | 15 | describe('Modal.vue', () => { 16 | let Wrapper 17 | let wrapper 18 | let store 19 | let state 20 | let mocks 21 | 22 | const createWrapper = mountMethod => { 23 | return () => { 24 | store = new Vuex.Store({ 25 | state, 26 | getters: { 27 | 'modal/open': getters.open, 28 | 'modal/data': getters.data 29 | }, 30 | mutations: { 31 | 'modal/SET_OPEN': mutations.SET_OPEN 32 | } 33 | }) 34 | return mountMethod(Modal, { store, mocks, localVue }) 35 | } 36 | } 37 | 38 | beforeEach(() => { 39 | mocks = { 40 | $filters: { 41 | truncate: a => a 42 | }, 43 | $toast: { 44 | success: () => {}, 45 | error: () => {} 46 | }, 47 | $t: () => {} 48 | } 49 | state = { 50 | open: null, 51 | data: {} 52 | } 53 | }) 54 | 55 | describe('shallowMount', () => { 56 | const Wrapper = createWrapper(shallowMount) 57 | 58 | it('initially empty', () => { 59 | wrapper = Wrapper() 60 | expect(wrapper.contains(DisableModal)).toBe(false) 61 | expect(wrapper.contains(ReportModal)).toBe(false) 62 | }) 63 | 64 | describe('store/modal holds data to disable', () => { 65 | beforeEach(() => { 66 | state = { 67 | open: 'disable', 68 | data: { 69 | type: 'contribution', 70 | resource: { 71 | id: 'c456', 72 | title: 'some title' 73 | } 74 | } 75 | } 76 | wrapper = Wrapper() 77 | }) 78 | 79 | it('renders disable modal', () => { 80 | expect(wrapper.contains(DisableModal)).toBe(true) 81 | }) 82 | 83 | it('passes data to disable modal', () => { 84 | expect(wrapper.find(DisableModal).props()).toEqual({ 85 | type: 'contribution', 86 | name: 'some title', 87 | id: 'c456' 88 | }) 89 | }) 90 | 91 | describe('child component emits close', () => { 92 | it('turns empty', () => { 93 | wrapper.find(DisableModal).vm.$emit('close') 94 | expect(wrapper.contains(DisableModal)).toBe(false) 95 | }) 96 | }) 97 | 98 | describe('store/modal data contains a comment', () => { 99 | it('passes author name to disable modal', () => { 100 | state.data = { 101 | type: 'comment', 102 | resource: { id: 'c456', author: { name: 'Author name' } } 103 | } 104 | wrapper = Wrapper() 105 | expect(wrapper.find(DisableModal).props()).toEqual({ 106 | type: 'comment', 107 | name: 'Author name', 108 | id: 'c456' 109 | }) 110 | }) 111 | 112 | it('does not crash if author is undefined', () => { 113 | state.data = { type: 'comment', resource: { id: 'c456' } } 114 | wrapper = Wrapper() 115 | expect(wrapper.find(DisableModal).props()).toEqual({ 116 | type: 'comment', 117 | name: '', 118 | id: 'c456' 119 | }) 120 | }) 121 | }) 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /components/Modal.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 60 | -------------------------------------------------------------------------------- /components/Modal/DisableModal.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 84 | -------------------------------------------------------------------------------- /components/Modal/ReportModal.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 113 | 114 | 128 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /components/ShoutButton.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 96 | 97 | 102 | -------------------------------------------------------------------------------- /components/User.spec.js: -------------------------------------------------------------------------------- 1 | import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils' 2 | import User from './User.vue' 3 | import Vue from 'vue' 4 | import Vuex from 'vuex' 5 | import VTooltip from 'v-tooltip' 6 | 7 | import Styleguide from '@human-connection/styleguide' 8 | 9 | const localVue = createLocalVue() 10 | const filter = jest.fn(str => str) 11 | 12 | localVue.use(Vuex) 13 | localVue.use(VTooltip) 14 | localVue.use(Styleguide) 15 | 16 | localVue.filter('truncate', filter) 17 | 18 | describe('User.vue', () => { 19 | let wrapper 20 | let Wrapper 21 | let propsData 22 | let mocks 23 | let stubs 24 | let getters 25 | let user 26 | 27 | beforeEach(() => { 28 | propsData = {} 29 | 30 | mocks = { 31 | $t: jest.fn() 32 | } 33 | stubs = { 34 | NuxtLink: RouterLinkStub 35 | } 36 | getters = { 37 | 'auth/user': () => { 38 | return {} 39 | }, 40 | 'auth/isModerator': () => false 41 | } 42 | }) 43 | 44 | describe('mount', () => { 45 | const Wrapper = () => { 46 | const store = new Vuex.Store({ 47 | getters 48 | }) 49 | return mount(User, { store, propsData, mocks, stubs, localVue }) 50 | } 51 | 52 | it('renders anonymous user', () => { 53 | const wrapper = Wrapper() 54 | expect(wrapper.text()).not.toMatch('Tilda Swinton') 55 | expect(wrapper.text()).toMatch('Anonymus') 56 | }) 57 | 58 | describe('given an user', () => { 59 | beforeEach(() => { 60 | propsData.user = { 61 | name: 'Tilda Swinton', 62 | slug: 'tilda-swinton' 63 | } 64 | }) 65 | 66 | it('renders user name', () => { 67 | const wrapper = Wrapper() 68 | expect(wrapper.text()).not.toMatch('Anonymous') 69 | expect(wrapper.text()).toMatch('Tilda Swinton') 70 | }) 71 | 72 | describe('user is disabled', () => { 73 | beforeEach(() => { 74 | propsData.user.disabled = true 75 | }) 76 | 77 | it('renders anonymous user', () => { 78 | const wrapper = Wrapper() 79 | expect(wrapper.text()).not.toMatch('Tilda Swinton') 80 | expect(wrapper.text()).toMatch('Anonymus') 81 | }) 82 | 83 | describe('current user is a moderator', () => { 84 | beforeEach(() => { 85 | getters['auth/isModerator'] = () => true 86 | }) 87 | 88 | it('renders user name', () => { 89 | const wrapper = Wrapper() 90 | expect(wrapper.text()).not.toMatch('Anonymous') 91 | expect(wrapper.text()).toMatch('Tilda Swinton') 92 | }) 93 | 94 | it('has "disabled-content" class', () => { 95 | const wrapper = Wrapper() 96 | expect(wrapper.classes()).toContain('disabled-content') 97 | }) 98 | }) 99 | }) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /components/mixins/seo.js: -------------------------------------------------------------------------------- 1 | export default { 2 | head() { 3 | return { 4 | htmlAttrs: { 5 | lang: this.$i18n.locale() 6 | }, 7 | bodyAttrs: { 8 | class: `page-name-${this.$route.name}` 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cypress.env.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "SEED_SERVER_HOST": "http://localhost:4001", 3 | "NEO4J_URI": "bolt://localhost:7687", 4 | "NEO4J_USERNAME": "neo4j", 5 | "NEO4J_PASSWORD": "letmein" 6 | } 7 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "qa7fe2", 3 | "ignoreTestFiles": "*.js", 4 | "baseUrl": "http://localhost:3000" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/fixtures/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin": { 3 | "email": "admin@example.org", 4 | "password": "1234", 5 | "name": "Peter Lustig" 6 | }, 7 | "moderator": { 8 | "email": "moderator@example.org", 9 | "password": "1234", 10 | "name": "Bob der Bausmeister" 11 | }, 12 | "user": { 13 | "email": "user@example.org", 14 | "password": "1234", 15 | "name": "Jenny Rostock" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cypress/integration/01.Login.feature: -------------------------------------------------------------------------------- 1 | Feature: Authentication 2 | As a database administrator 3 | I want users to sign in 4 | In order to attribute posts and other contributions to their authors 5 | 6 | Background: 7 | Given I have a user account 8 | 9 | Scenario: Log in 10 | When I visit the "/login" page 11 | And I fill in my email and password combination and click submit 12 | Then I can click on my profile picture in the top right corner 13 | And I can see my name "Peter Lustig" in the dropdown menu 14 | 15 | Scenario: Refresh and stay logged in 16 | Given I am logged in 17 | When I refresh the page 18 | Then I am still logged in 19 | 20 | Scenario: Log out 21 | Given I am logged in 22 | When I log out through the menu in the top right corner 23 | Then I see the login screen again 24 | -------------------------------------------------------------------------------- /cypress/integration/02.Internationalization.feature: -------------------------------------------------------------------------------- 1 | Feature: Internationalization 2 | As a user who is not very fluent in English 3 | I would like to see the user interface translated to my preferred language 4 | In order to be able to understand the interface 5 | 6 | Background: 7 | Given I am on the "login" page 8 | 9 | Scenario Outline: I select "" in the language menu and see "" 10 | When I select "" in the language menu 11 | Then the whole user interface appears in "" 12 | Then I see a button with the label "" 13 | 14 | Examples: Login Button 15 | | language | buttonLabel | 16 | | Français | Connexion | 17 | | Deutsch | Einloggen | 18 | | English | Login | 19 | 20 | Scenario: Keep preferred language after refresh 21 | Given I previously switched the language to "Français" 22 | And I refresh the page 23 | Then the whole user interface appears in "Français" 24 | -------------------------------------------------------------------------------- /cypress/integration/03.TagsAndCategories.feature: -------------------------------------------------------------------------------- 1 | Feature: Tags and Categories 2 | As a database administrator 3 | I would like to see a summary of all tags and categories and their usage 4 | In order to be able to decide which tags and categories are popular or not 5 | 6 | The currently deployed application, codename "Alpha", distinguishes between 7 | categories and tags. Each post can have a number of categories and/or tags. 8 | A few categories are required for each post, tags are completely optional. 9 | Both help to find relevant posts in the database, e.g. users can filter for 10 | categories. 11 | 12 | If administrators summary of all tags and categories and how often they are 13 | used, they learn what new category might be convenient for users, e.g. by 14 | looking at the popularity of a tag. 15 | 16 | Background: 17 | Given my user account has the role "admin" 18 | And we have a selection of tags and categories as well as posts 19 | And I am logged in 20 | 21 | Scenario: See an overview of categories 22 | When I navigate to the administration dashboard 23 | And I click on the menu item "Categories" 24 | Then I can see a list of categories ordered by post count: 25 | | Icon | Name | Posts | 26 | | | Just For Fun | 2 | 27 | | | Happyness & Values | 1 | 28 | | | Health & Wellbeing | 0 | 29 | 30 | Scenario: See an overview of tags 31 | When I navigate to the administration dashboard 32 | And I click on the menu item "Tags" 33 | Then I can see a list of tags ordered by user count: 34 | | # | Name | Users | Posts | 35 | | 1 | Democracy | 2 | 3 | 36 | | 2 | Ecology | 1 | 1 | 37 | | 3 | Nature | 1 | 2 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /cypress/integration/04.AboutMeAndLocation.feature: -------------------------------------------------------------------------------- 1 | Feature: About me and location 2 | As a user 3 | I would like to add some about me text and a location 4 | So others can get some info about me and my location 5 | 6 | The location and about me are displayed on the user profile. Later it will be possible 7 | to search for users by location. 8 | 9 | Background: 10 | Given I have a user account 11 | And I am logged in 12 | And I am on the "settings" page 13 | 14 | Scenario: Change username 15 | When I save "Hansi" as my new name 16 | Then I can see my new name "Hansi" when I click on my profile picture in the top right 17 | And when I refresh the page 18 | Then the name "Hansi" is still there 19 | 20 | Scenario Outline: I set my location to "" 21 | When I save "" as my location 22 | When people visit my profile page 23 | Then they can see the location in the info box below my avatar 24 | 25 | Examples: Location 26 | | location | type | 27 | | Paris | City | 28 | | Saxony-Anhalt | Region | 29 | | Germany | Country | 30 | 31 | Scenario: Display a description on profile page 32 | Given I have the following self-description: 33 | """ 34 | Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei 35 | """ 36 | When people visit my profile page 37 | Then they can see the text in the info box below my avatar 38 | -------------------------------------------------------------------------------- /cypress/integration/06.Search.feature: -------------------------------------------------------------------------------- 1 | Feature: Search 2 | As a user 3 | I would like to be able to search for specific words 4 | In order to find related content 5 | 6 | Background: 7 | Given I have a user account 8 | And we have the following posts in our database: 9 | | Author | id | title | content | 10 | | Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! | 11 | | Brianna Wiest | p1 | No searched for content | will be found in this post, I guarantee | 12 | Given I am logged in 13 | 14 | Scenario: Search for specific words 15 | When I search for "Essays" 16 | Then I should have one post in the select dropdown 17 | Then I should see the following posts in the select dropdown: 18 | | title | 19 | | 101 Essays that will change the way you think | 20 | 21 | Scenario: Press enter starts search 22 | When I type "Essa" and press Enter 23 | Then I should have one post in the select dropdown 24 | Then I should see the following posts in the select dropdown: 25 | | title | 26 | | 101 Essays that will change the way you think | 27 | 28 | Scenario: Press escape clears search 29 | When I type "Ess" and press escape 30 | Then the search field should clear 31 | 32 | Scenario: Select entry goes to post 33 | When I search for "Essays" 34 | And I select an entry 35 | Then I should be on the post's page 36 | 37 | Scenario: Select dropdown content 38 | When I search for "Essays" 39 | Then I should have one post in the select dropdown 40 | Then I should see posts with the searched-for term in the select dropdown 41 | And I should not see posts without the searched-for term in the select dropdown 42 | -------------------------------------------------------------------------------- /cypress/integration/06.WritePost.feature: -------------------------------------------------------------------------------- 1 | Feature: Create a post 2 | As a user 3 | I would like to create a post 4 | To say something to everyone in the community 5 | 6 | Background: 7 | Given I have a user account 8 | And I am logged in 9 | And I am on the "landing" page 10 | 11 | Scenario: Create a post 12 | When I click on the big plus icon in the bottom right corner to create post 13 | And I choose "My first post" as the title of the post 14 | And I type in the following text: 15 | """ 16 | Human Connection is a free and open-source social network 17 | for active citizenship. 18 | """ 19 | And I click on "Save" 20 | Then I get redirected to "/post/my-first-post/" 21 | And the post was saved successfully 22 | 23 | Scenario: See a post on the landing page 24 | Given I previously created a post 25 | Then the post shows up on the landing page at position 1 26 | -------------------------------------------------------------------------------- /cypress/integration/common/admin.js: -------------------------------------------------------------------------------- 1 | import { When, Then } from 'cypress-cucumber-preprocessor/steps' 2 | 3 | /* global cy */ 4 | 5 | When('I navigate to the administration dashboard', () => { 6 | cy.get('.avatar-menu').click() 7 | cy.get('.avatar-menu-popover') 8 | .find('a[href="/admin"]') 9 | .click() 10 | }) 11 | 12 | Then('I can see a list of categories ordered by post count:', table => { 13 | cy.get('thead') 14 | .find('tr th') 15 | .should('have.length', 3) 16 | table.hashes().forEach(({ Name, Posts }, index) => { 17 | cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`).should( 18 | 'contain', 19 | Name.trim() 20 | ) 21 | cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`).should( 22 | 'contain', 23 | Posts 24 | ) 25 | }) 26 | }) 27 | 28 | Then('I can see a list of tags ordered by user count:', table => { 29 | cy.get('thead') 30 | .find('tr th') 31 | .should('have.length', 4) 32 | table.hashes().forEach(({ Name, Users, Posts }, index) => { 33 | cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`).should( 34 | 'contain', 35 | Name.trim() 36 | ) 37 | cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`).should( 38 | 'contain', 39 | Users 40 | ) 41 | cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(4)`).should( 42 | 'contain', 43 | Posts 44 | ) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /cypress/integration/common/search.js: -------------------------------------------------------------------------------- 1 | import { When, Then } from 'cypress-cucumber-preprocessor/steps' 2 | When('I search for {string}', value => { 3 | cy.get('#nav-search') 4 | .focus() 5 | .type(value) 6 | }) 7 | 8 | Then('I should have one post in the select dropdown', () => { 9 | cy.get('.ds-select-dropdown').should($li => { 10 | expect($li).to.have.length(1) 11 | }) 12 | }) 13 | 14 | Then('I should see the following posts in the select dropdown:', table => { 15 | table.hashes().forEach(({ title }) => { 16 | cy.get('.ds-select-dropdown').should('contain', title) 17 | }) 18 | }) 19 | 20 | When('I type {string} and press Enter', value => { 21 | cy.get('#nav-search') 22 | .focus() 23 | .type(value) 24 | .type('{enter}', { force: true }) 25 | }) 26 | 27 | When('I type {string} and press escape', value => { 28 | cy.get('#nav-search') 29 | .focus() 30 | .type(value) 31 | .type('{esc}') 32 | }) 33 | 34 | Then('the search field should clear', () => { 35 | cy.get('#nav-search').should('have.text', '') 36 | }) 37 | 38 | When('I select an entry', () => { 39 | cy.get('.ds-select-dropdown ul li') 40 | .first() 41 | .trigger('click') 42 | }) 43 | 44 | Then("I should be on the post's page", () => { 45 | cy.location('pathname').should( 46 | 'eq', 47 | '/post/101-essays-that-will-change-the-way-you-think/' 48 | ) 49 | }) 50 | 51 | Then( 52 | 'I should see posts with the searched-for term in the select dropdown', 53 | () => { 54 | cy.get('.ds-select-dropdown').should( 55 | 'contain', 56 | '101 Essays that will change the way you think' 57 | ) 58 | } 59 | ) 60 | 61 | Then( 62 | 'I should not see posts without the searched-for term in the select dropdown', 63 | () => { 64 | cy.get('.ds-select-dropdown').should( 65 | 'not.contain', 66 | 'No searched for content' 67 | ) 68 | } 69 | ) 70 | -------------------------------------------------------------------------------- /cypress/integration/common/settings.js: -------------------------------------------------------------------------------- 1 | import { When, Then } from 'cypress-cucumber-preprocessor/steps' 2 | 3 | /* global cy */ 4 | 5 | let aboutMeText 6 | let myLocation 7 | 8 | const matchNameInUserMenu = name => { 9 | cy.get('.avatar-menu').click() // open 10 | cy.get('.avatar-menu-popover').contains(name) 11 | cy.get('.avatar-menu').click() // close again 12 | } 13 | 14 | When('I save {string} as my new name', name => { 15 | cy.get('input[id=name]') 16 | .clear() 17 | .type(name) 18 | cy.get('[type=submit]') 19 | .click() 20 | .not('[disabled]') 21 | }) 22 | 23 | When('I save {string} as my location', location => { 24 | cy.get('input[id=city]').type(location) 25 | cy.get('.ds-select-option') 26 | .contains(location) 27 | .click() 28 | cy.get('[type=submit]') 29 | .click() 30 | .not('[disabled]') 31 | myLocation = location 32 | }) 33 | 34 | When('I have the following self-description:', text => { 35 | cy.get('textarea[id=bio]') 36 | .clear() 37 | .type(text) 38 | cy.get('[type=submit]') 39 | .click() 40 | .not('[disabled]') 41 | aboutMeText = text 42 | }) 43 | 44 | When('people visit my profile page', url => { 45 | cy.openPage('/profile/peter-pan') 46 | }) 47 | 48 | When('they can see the text in the info box below my avatar', () => { 49 | cy.contains(aboutMeText) 50 | }) 51 | 52 | Then('they can see the location in the info box below my avatar', () => { 53 | cy.contains(myLocation) 54 | }) 55 | 56 | Then('the name {string} is still there', name => { 57 | matchNameInUserMenu(name) 58 | }) 59 | 60 | Then( 61 | 'I can see my new name {string} when I click on my profile picture in the top right', 62 | name => matchNameInUserMenu(name) 63 | ) 64 | -------------------------------------------------------------------------------- /cypress/integration/moderation/HidePosts.feature: -------------------------------------------------------------------------------- 1 | Feature: Hide Posts 2 | As the moderator team 3 | we'd like to be able to hide posts from the public 4 | to enforce our network's code of conduct and/or legal regulations 5 | 6 | Background: 7 | Given we have the following posts in our database: 8 | | id | title | deleted | disabled | 9 | | p1 | This post should be visible | | | 10 | | p2 | This post is disabled | | x | 11 | | p3 | This post is deleted | x | | 12 | 13 | Scenario: Disabled posts don't show up on the landing page 14 | Given I am logged in with a "user" role 15 | Then I should see only 1 post on the landing page 16 | And the first post on the landing page has the title: 17 | """ 18 | This post should be visible 19 | """ 20 | 21 | Scenario: Visiting a disabled post's page should return 404 22 | Given I am logged in with a "user" role 23 | Then the page "/post/this-post-is-disabled" returns a 404 error with a message: 24 | """ 25 | This post could not be found 26 | """ 27 | -------------------------------------------------------------------------------- /cypress/integration/moderation/ReportContent.feature: -------------------------------------------------------------------------------- 1 | Feature: Report and Moderate 2 | As a user 3 | I would like to report content that viloates the community guidlines 4 | So the moderators can take action on it 5 | 6 | As a moderator 7 | I would like to see all reported content 8 | So I can look into it and decide what to do 9 | 10 | Background: 11 | Given we have the following posts in our database: 12 | | Author | id | title | content | 13 | | David Irving | p1 | The Truth about the Holocaust | It never existed! | 14 | 15 | Scenario Outline: Report a post from various pages 16 | Given I am logged in with a "user" role 17 | When I see David Irving's post on the 18 | And I click on "Report Post" from the triple dot menu of the post 19 | And I confirm the reporting dialog because it is a criminal act under German law: 20 | """ 21 | Do you really want to report the contribution "The Truth about the Holocaust"? 22 | """ 23 | Then I see a success message: 24 | """ 25 | Thanks for reporting! 26 | """ 27 | Examples: 28 | | Page | 29 | | landing page | 30 | | post page | 31 | 32 | Scenario: Report user 33 | Given I am logged in with a "user" role 34 | And I see David Irving's post on the post page 35 | When I click on the author 36 | And I click on "Report User" from the triple dot menu in the user info box 37 | And I confirm the reporting dialog because he is a holocaust denier: 38 | """ 39 | Do you really want to report the user "David Irving"? 40 | """ 41 | Then I see a success message: 42 | """ 43 | Thanks for reporting! 44 | """ 45 | 46 | Scenario: Review reported content 47 | Given somebody reported the following posts: 48 | | id | 49 | | p1 | 50 | And I am logged in with a "moderator" role 51 | When I click on the avatar menu in the top right corner 52 | And I click on "Moderation" 53 | Then I see all the reported posts including the one from above 54 | And each list item links to the post page 55 | 56 | Scenario: Normal user can't see the moderation page 57 | Given I am logged in with a "user" role 58 | When I click on the avatar menu in the top right corner 59 | Then I can't see the moderation menu item 60 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const cucumber = require('cypress-cucumber-preprocessor').default 15 | module.exports = on => { 16 | // (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | on('file:preprocessor', cucumber()) 20 | } 21 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | 15 | /* globals Cypress cy */ 16 | 17 | import { getLangByName } from './helpers' 18 | import users from '../fixtures/users.json' 19 | 20 | const switchLang = name => { 21 | cy.get('.locale-menu').click() 22 | cy.contains('.locale-menu-popover a', name).click() 23 | } 24 | 25 | Cypress.Commands.add('switchLanguage', (name, force) => { 26 | const code = getLangByName(name).code 27 | if (force) { 28 | switchLang(name) 29 | } else { 30 | cy.get('html').then($html => { 31 | if ($html && $html.attr('lang') !== code) { 32 | switchLang(name) 33 | } 34 | }) 35 | } 36 | }) 37 | 38 | Cypress.Commands.add('login', ({ email, password }) => { 39 | cy.visit(`/login`) 40 | cy.get('input[name=email]') 41 | .trigger('focus') 42 | .type(email) 43 | cy.get('input[name=password]') 44 | .trigger('focus') 45 | .type(password) 46 | cy.get('button[name=submit]') 47 | .as('submitButton') 48 | .click() 49 | cy.location('pathname').should('eq', '/') // we're in! 50 | }) 51 | 52 | Cypress.Commands.add('logout', (email, password) => { 53 | cy.visit(`/logout`) 54 | cy.location('pathname').should('contain', '/login') // we're out 55 | }) 56 | 57 | Cypress.Commands.add('openPage', page => { 58 | if (page === 'landing') { 59 | page = '' 60 | } 61 | cy.visit(`/${page}`) 62 | }) 63 | 64 | // 65 | // 66 | // -- This is a child command -- 67 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 68 | // 69 | // 70 | // -- This is a dual command -- 71 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 72 | // 73 | // 74 | // -- This is will overwrite an existing command -- 75 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 76 | -------------------------------------------------------------------------------- /cypress/support/factories.js: -------------------------------------------------------------------------------- 1 | // TODO: find a better way how to import the factories 2 | import Factory from '../../../Nitro-Backend/src/seed/factories' 3 | import { getDriver } from '../../../Nitro-Backend/src/bootstrap/neo4j' 4 | 5 | const neo4jDriver = getDriver({ 6 | uri: Cypress.env('NEO4J_URI'), 7 | username: Cypress.env('NEO4J_USERNAME'), 8 | password: Cypress.env('NEO4J_PASSWORD') 9 | }) 10 | const factory = Factory({ neo4jDriver }) 11 | const seedServerHost = Cypress.env('SEED_SERVER_HOST') 12 | 13 | beforeEach(async () => { 14 | await factory.cleanDatabase({ seedServerHost, neo4jDriver }) 15 | }) 16 | 17 | Cypress.Commands.add('factory', () => { 18 | return Factory({ seedServerHost }) 19 | }) 20 | 21 | Cypress.Commands.add( 22 | 'create', 23 | { prevSubject: true }, 24 | (factory, node, properties) => { 25 | return factory.create(node, properties) 26 | } 27 | ) 28 | 29 | Cypress.Commands.add( 30 | 'relate', 31 | { prevSubject: true }, 32 | (factory, node, relationship, properties) => { 33 | return factory.relate(node, relationship, properties) 34 | } 35 | ) 36 | 37 | Cypress.Commands.add( 38 | 'mutate', 39 | { prevSubject: true }, 40 | (factory, mutation, variables) => { 41 | return factory.mutate(mutation, variables) 42 | } 43 | ) 44 | 45 | Cypress.Commands.add( 46 | 'authenticateAs', 47 | { prevSubject: true }, 48 | (factory, loginCredentials) => { 49 | return factory.authenticateAs(loginCredentials) 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /cypress/support/helpers.js: -------------------------------------------------------------------------------- 1 | import find from 'lodash/find' 2 | 3 | const helpers = { 4 | locales: require('../../locales'), 5 | getLangByName: name => { 6 | return find(helpers.locales, { name }) 7 | } 8 | } 9 | 10 | export default helpers 11 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | import './factories' 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | webapp: 5 | build: 6 | context: . 7 | target: build-and-test 8 | volumes: 9 | - .:/nitro-web 10 | - node_modules:/nitro-web/node_modules 11 | - nuxt:/nitro-web/.nuxt 12 | command: yarn run dev 13 | 14 | volumes: 15 | node_modules: 16 | nuxt: 17 | -------------------------------------------------------------------------------- /docker-compose.travis.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | webapp: 5 | build: 6 | context: . 7 | target: build-and-test 8 | environment: 9 | - BACKEND_URL=http://backend:4123 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | webapp: 5 | image: humanconnection/nitro-web:latest 6 | build: 7 | context: . 8 | target: production 9 | ports: 10 | - 3000:3000 11 | - 8080:8080 12 | networks: 13 | - hc-network 14 | environment: 15 | - HOST=0.0.0.0 16 | - BACKEND_URL=http://backend:4000 17 | - MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ" 18 | 19 | networks: 20 | hc-network: 21 | name: hc-network 22 | 23 | volumes: 24 | node_modules: 25 | -------------------------------------------------------------------------------- /graphql/ModerationListQuery.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | 3 | export default app => { 4 | return gql(` 5 | query { 6 | Report(first: 20, orderBy: createdAt_desc) { 7 | id 8 | description 9 | type 10 | createdAt 11 | submitter { 12 | disabled 13 | deleted 14 | name 15 | slug 16 | } 17 | user { 18 | name 19 | slug 20 | disabled 21 | deleted 22 | disabledBy { 23 | slug 24 | name 25 | } 26 | } 27 | comment { 28 | contentExcerpt 29 | author { 30 | name 31 | slug 32 | disabled 33 | deleted 34 | } 35 | post { 36 | disabled 37 | deleted 38 | title 39 | slug 40 | } 41 | disabledBy { 42 | disabled 43 | deleted 44 | slug 45 | name 46 | } 47 | } 48 | post { 49 | title 50 | slug 51 | disabled 52 | deleted 53 | author { 54 | disabled 55 | deleted 56 | name 57 | slug 58 | } 59 | disabledBy { 60 | disabled 61 | deleted 62 | slug 63 | name 64 | } 65 | } 66 | } 67 | } 68 | `) 69 | } 70 | -------------------------------------------------------------------------------- /graphql/PostMutations.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | 3 | export default app => { 4 | return { 5 | CreatePost: gql(` 6 | mutation($title: String!, $content: String!) { 7 | CreatePost(title: $title, content: $content) { 8 | id 9 | title 10 | slug 11 | content 12 | contentExcerpt 13 | } 14 | } 15 | `), 16 | UpdatePost: gql(` 17 | mutation($id: ID!, $title: String!, $content: String!) { 18 | UpdatePost(id: $id, title: $title, content: $content) { 19 | id 20 | title 21 | slug 22 | content 23 | contentExcerpt 24 | } 25 | } 26 | `) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /graphql/UserProfileQuery.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | 3 | export default app => { 4 | const lang = app.$i18n.locale().toUpperCase() 5 | return gql(` 6 | query User($slug: String!, $first: Int, $offset: Int) { 7 | User(slug: $slug) { 8 | id 9 | name 10 | avatar 11 | about 12 | disabled 13 | deleted 14 | locationName 15 | location { 16 | name: name${lang} 17 | } 18 | createdAt 19 | badges { 20 | id 21 | key 22 | icon 23 | } 24 | badgesCount 25 | shoutedCount 26 | commentsCount 27 | followingCount 28 | following(first: 7) { 29 | id 30 | name 31 | slug 32 | avatar 33 | disabled 34 | deleted 35 | followedByCount 36 | followedByCurrentUser 37 | contributionsCount 38 | commentsCount 39 | badges { 40 | id 41 | key 42 | icon 43 | } 44 | location { 45 | name: name${lang} 46 | } 47 | } 48 | followedByCount 49 | followedByCurrentUser 50 | followedBy(first: 7) { 51 | id 52 | name 53 | disabled 54 | deleted 55 | slug 56 | avatar 57 | followedByCount 58 | followedByCurrentUser 59 | contributionsCount 60 | commentsCount 61 | badges { 62 | id 63 | key 64 | icon 65 | } 66 | location { 67 | name: name${lang} 68 | } 69 | } 70 | contributionsCount 71 | contributions(first: $first, offset: $offset, orderBy: createdAt_desc) { 72 | id 73 | slug 74 | title 75 | contentExcerpt 76 | shoutedCount 77 | commentsCount 78 | deleted 79 | image 80 | createdAt 81 | disabled 82 | deleted 83 | categories { 84 | id 85 | name 86 | icon 87 | } 88 | author { 89 | id 90 | avatar 91 | name 92 | disabled 93 | deleted 94 | location { 95 | name: name${lang} 96 | } 97 | } 98 | } 99 | } 100 | } 101 | `) 102 | } 103 | -------------------------------------------------------------------------------- /layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /layouts/blank.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": { 3 | "copy": "Si ya tiene una cuenta de Human Connection, inicie sesión aquí.", 4 | "login": "Iniciar sesión", 5 | "logout": "Cierre de sesión", 6 | "email": "Tu correo electrónico", 7 | "password": "Tu contraseña", 8 | "moreInfo": "¿Qué es Human Connection?", 9 | "hello": "Hola" 10 | }, 11 | "profile": { 12 | "name": "Mi perfil", 13 | "memberSince": "Miembro desde", 14 | "follow": "Seguir", 15 | "followers": "Seguidores", 16 | "following": "Siguiendo", 17 | "shouted": "Gritar", 18 | "commented": "Comentado" 19 | }, 20 | "settings": { 21 | "name": "Configuración", 22 | "data": { 23 | "name": "Sus datos" 24 | }, 25 | "security": { 26 | "name": "Seguridad" 27 | }, 28 | "invites": { 29 | "name": "Invita" 30 | }, 31 | "download": { 32 | "name": "Descargar datos" 33 | }, 34 | "delete": { 35 | "name": "Borrar cuenta" 36 | }, 37 | "organizations": { 38 | "name": "Mis organizaciones" 39 | }, 40 | "languages": { 41 | "name": "Idiomas" 42 | } 43 | }, 44 | "admin": { 45 | "name": "Admin", 46 | "dashboard": { 47 | "name": "Tablero", 48 | "users": "Usuarios", 49 | "posts": "Mensajes", 50 | "comments": "Comentarios", 51 | "notifications": "Notificaciones", 52 | "organizations": "Organizaciones", 53 | "projects": "Proyectos", 54 | "invites": "Invita", 55 | "follows": "Sigue", 56 | "shouts": "Gritos" 57 | }, 58 | "organizations": { 59 | "name": "Organizaciones" 60 | }, 61 | "users": { 62 | "name": "Usuarios" 63 | }, 64 | "pages": { 65 | "name": "Páginas" 66 | }, 67 | "notifications": { 68 | "name": "Notificaciones" 69 | }, 70 | "categories": { 71 | "name": "Categorías", 72 | "categoryName": "Nombre", 73 | "postCount": "Mensajes" 74 | }, 75 | "tags": { 76 | "name": "Etiquetas", 77 | "tagCountUnique": "Usuarios", 78 | "tagCount": "Mensajes" 79 | }, 80 | "settings": { 81 | "name": "Configuración" 82 | } 83 | }, 84 | "post": { 85 | "name": "Mensaje", 86 | "moreInfo": { 87 | "name": "Más info" 88 | }, 89 | "takeAction": { 90 | "name": "Tomar acción" 91 | } 92 | }, 93 | "quotes": { 94 | "african": { 95 | "quote": "Muchas personas pequeñas en muchos lugares pequeños hacen muchas cosas pequeñas, que pueden alterar la faz del mundo.", 96 | "author": "Proverbio africano" 97 | } 98 | }, 99 | "common": { 100 | "post": "Mensaje ::: Mensajes", 101 | "comment": "Comentario ::: Comentarios", 102 | "letsTalk": "Hablemos", 103 | "versus": "Versus", 104 | "moreInfo": "Más info", 105 | "takeAction": "Tomar acción", 106 | "shout": "Grito ::: Gritos", 107 | "user": "Usuario ::: Usuarios", 108 | "category": "Categoría ::: Categorías", 109 | "organization": "Organización ::: Organizaciones", 110 | "project": "Proyecto ::: Proyectos", 111 | "tag": "Etiqueta ::: Etiquetas", 112 | "name": "Nombre", 113 | "loadMore": "cargar más", 114 | "loading": "cargando" 115 | } 116 | } -------------------------------------------------------------------------------- /locales/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: 'English', 4 | code: 'en', 5 | iso: 'en-US', 6 | enabled: true 7 | }, 8 | { 9 | name: 'Deutsch', 10 | code: 'de', 11 | iso: 'de-DE', 12 | enabled: true 13 | }, 14 | { 15 | name: 'Nederlands', 16 | code: 'nl', 17 | iso: 'nl-NL', 18 | enabled: true 19 | }, 20 | { 21 | name: 'Français', 22 | code: 'fr', 23 | iso: 'fr-FR', 24 | enabled: true 25 | }, 26 | { 27 | name: 'Italiano', 28 | code: 'it', 29 | iso: 'it-IT', 30 | enabled: true 31 | }, 32 | { 33 | name: 'Español', 34 | code: 'es', 35 | iso: 'es-ES', 36 | enabled: true 37 | }, 38 | { 39 | name: 'Português', 40 | code: 'pt', 41 | iso: 'pt-PT', 42 | enabled: true 43 | }, 44 | { 45 | name: 'Polski', 46 | code: 'pl', 47 | iso: 'pl-PL', 48 | enabled: true 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": { 3 | "copy": "Se sei gia registrato su Human Connection, accedi qui.", 4 | "login": "Accesso", 5 | "logout": "Logout", 6 | "email": "La tua email", 7 | "password": "La tua password", 8 | "moreInfo": "Che cosa è Human Connection?", 9 | "hello": "Ciao" 10 | }, 11 | "profile": { 12 | "name": "Il mio profilo", 13 | "memberSince": "Membro dal", 14 | "follow": "Seguire", 15 | "followers": "Seguenti", 16 | "following": "Seguendo", 17 | "shouted": "Gridato", 18 | "commented": "Commentato" 19 | }, 20 | "settings": { 21 | "name": "Impostazioni", 22 | "data": { 23 | "name": "I tuoi dati", 24 | "labelName": "Nome", 25 | "labelCity": "La tua città o regione", 26 | "labelBio": "Su di te" 27 | }, 28 | "security": { 29 | "name": "Sicurezza" 30 | }, 31 | "invites": { 32 | "name": "Inviti" 33 | }, 34 | "download": { 35 | "name": "Scaricamento dati" 36 | }, 37 | "delete": { 38 | "name": "Elimina Account" 39 | }, 40 | "organizations": { 41 | "name": "Mie organizzazioni" 42 | }, 43 | "languages": { 44 | "name": "Lingue" 45 | } 46 | }, 47 | "admin": { 48 | "name": "Admin", 49 | "dashboard": { 50 | "name": "Cruscotto", 51 | "users": "Utenti", 52 | "posts": "Messaggi", 53 | "comments": "Commenti", 54 | "notifications": "Notifiche", 55 | "organizations": "Organizzazioni", 56 | "projects": "Progetti", 57 | "invites": "Inviti", 58 | "follows": "Segue", 59 | "shouts": "Gridi" 60 | }, 61 | "organizations": { 62 | "name": "Organizzazioni" 63 | }, 64 | "users": { 65 | "name": "Utenti" 66 | }, 67 | "pages": { 68 | "name": "Pagine" 69 | }, 70 | "notifications": { 71 | "name": "Notifiche" 72 | }, 73 | "categories": { 74 | "name": "Categorie", 75 | "categoryName": "Nome", 76 | "postCount": "Messaggi" 77 | }, 78 | "tags": { 79 | "name": "Tag", 80 | "tagCountUnique": "Utenti", 81 | "tagCount": "Messaggi" 82 | }, 83 | "settings": { 84 | "name": "Impostazioni" 85 | } 86 | }, 87 | "post": { 88 | "name": "Messaggio", 89 | "moreInfo": { 90 | "name": "Ulteriori informazioni" 91 | }, 92 | "takeAction": { 93 | "name": "Agire" 94 | } 95 | }, 96 | "quotes": { 97 | "african": { 98 | "quote": "Molte piccole persone in molti piccoli luoghi fanno molte piccole cose, che possono cambiare la faccia del mondo.", 99 | "author": "Proverbio africano" 100 | } 101 | }, 102 | "common": { 103 | "post": "Messaggio ::: Messaggi", 104 | "comment": "Commento ::: Commenti", 105 | "letsTalk": "Discutiamo", 106 | "versus": "Verso", 107 | "moreInfo": "Ulteriori informazioni", 108 | "takeAction": "Agire", 109 | "shout": "Grido ::: Gridi", 110 | "user": "Utente ::: Utenti", 111 | "category": "Categoria ::: Categorie", 112 | "organization": "Organizzazione ::: Organizzazioni", 113 | "project": "Progetto ::: Progetti", 114 | "tag": "Tag ::: Tag", 115 | "name": "Nome", 116 | "loadMore": "Caricare di più", 117 | "loading": "Caricamento in corso" 118 | }, 119 | "actions": { 120 | "loading": "Caricamento in corso", 121 | "loadMore": "Carica di più", 122 | "create": "Crea", 123 | "save": "Salva", 124 | "edit": "Modifica", 125 | "delete": "Cancella" 126 | } 127 | } -------------------------------------------------------------------------------- /lokalise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/lokalise.png -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | The middleware lets you define custom function to be ran before rendering a page or a group of pages (layouts). 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /middleware/authenticated.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty' 2 | 3 | export default async ({ store, env, route, redirect }) => { 4 | let publicPages = env.publicPages 5 | // only affect non public pages 6 | if (publicPages.indexOf(route.name) >= 0) { 7 | return true 8 | } 9 | 10 | // await store.dispatch('auth/refreshJWT', 'authenticated middleware') 11 | const isAuthenticated = await store.dispatch('auth/check') 12 | if (isAuthenticated === true) { 13 | return true 14 | } 15 | 16 | // try to logout user 17 | // await store.dispatch('auth/logout', null, { root: true }) 18 | 19 | // set the redirect path for after the login 20 | let params = {} 21 | if (!isEmpty(route.path) && route.path !== '/') { 22 | params.path = route.path 23 | } 24 | 25 | return redirect('/login', params) 26 | } 27 | -------------------------------------------------------------------------------- /middleware/isAdmin.js: -------------------------------------------------------------------------------- 1 | export default ({ store, error }) => { 2 | if (!store.getters['auth/isAdmin']) { 3 | return error({ statusCode: 403 }) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /middleware/isModerator.js: -------------------------------------------------------------------------------- 1 | export default ({ store, error }) => { 2 | if (!store.getters['auth/isModerator']) { 3 | return error({ statusCode: 403 }) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hc-webapp-next", 3 | "version": "1.0.0", 4 | "description": "Human Connection GraphQL UI Prototype", 5 | "author": "Grzegorz Leoniec", 6 | "private": true, 7 | "scripts": { 8 | "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server", 9 | "dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn dev", 10 | "build": "nuxt build", 11 | "start": "cross-env node server/index.js", 12 | "generate": "nuxt generate", 13 | "lint": "eslint --ext .js,.vue .", 14 | "test": "jest", 15 | "precommit": "yarn lint", 16 | "e2e:local": "cypress run --headed", 17 | "e2e:ci": "npm-run-all --parallel --race start:ci 'cypress:ci --config baseUrl=http://localhost:3000'", 18 | "test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --no-cache --runInBand" 19 | }, 20 | "cypress-cucumber-preprocessor": { 21 | "nonGlobalStepDefinitions": true 22 | }, 23 | "jest": { 24 | "verbose": true, 25 | "moduleFileExtensions": [ 26 | "js", 27 | "json", 28 | "vue" 29 | ], 30 | "transform": { 31 | ".*\\.(vue)$": "vue-jest", 32 | "^.+\\.js$": "/node_modules/babel-jest" 33 | }, 34 | "moduleNameMapper": { 35 | "^@/(.*)$": "/src/$1", 36 | "^~/(.*)$": "/$1" 37 | } 38 | }, 39 | "dependencies": { 40 | "@human-connection/styleguide": "0.5.15", 41 | "@nuxtjs/apollo": "4.0.0-rc4", 42 | "@nuxtjs/axios": "~5.4.1", 43 | "@nuxtjs/dotenv": "~1.3.0", 44 | "@nuxtjs/style-resources": "~0.1.2", 45 | "accounting": "~0.4.1", 46 | "apollo-cache-inmemory": "~1.5.1", 47 | "apollo-client": "~2.5.1", 48 | "cookie-universal-nuxt": "~2.0.14", 49 | "cross-env": "~5.2.0", 50 | "date-fns": "2.0.0-alpha.27", 51 | "express": "~4.16.4", 52 | "graphql": "~14.1.1", 53 | "jsonwebtoken": "~8.5.1", 54 | "linkify-it": "~2.1.0", 55 | "nuxt": "~2.4.5", 56 | "nuxt-env": "~0.1.0", 57 | "string-hash": "^1.1.3", 58 | "tiptap": "^1.14.0", 59 | "tiptap-extensions": "^1.14.0", 60 | "v-tooltip": "~2.0.0-rc.33", 61 | "vue-count-to": "~1.0.13", 62 | "vue-izitoast": "1.1.2", 63 | "vue-sweetalert-icons": "~3.2.0", 64 | "vuex-i18n": "~1.11.0" 65 | }, 66 | "devDependencies": { 67 | "@babel/core": "~7.3.4", 68 | "@babel/preset-env": "~7.3.4", 69 | "@vue/cli-shared-utils": "~3.4.1", 70 | "@vue/eslint-config-prettier": "~4.0.1", 71 | "@vue/server-test-utils": "~1.0.0-beta.29", 72 | "@vue/test-utils": "~1.0.0-beta.29", 73 | "babel-core": "~7.0.0-bridge.0", 74 | "babel-eslint": "~10.0.1", 75 | "babel-jest": "~24.5.0", 76 | "cypress-cucumber-preprocessor": "~1.11.0", 77 | "eslint": "~5.15.1", 78 | "eslint-config-prettier": "~3.6.0", 79 | "eslint-loader": "~2.1.2", 80 | "eslint-plugin-prettier": "~3.0.1", 81 | "eslint-plugin-vue": "~5.2.2", 82 | "jest": "~24.5.0", 83 | "node-sass": "~4.11.0", 84 | "nodemon": "~1.18.10", 85 | "prettier": "~1.14.3", 86 | "sass-loader": "~7.1.0", 87 | "vue-jest": "~3.0.4", 88 | "vue-svg-loader": "~0.11.0" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and create the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /pages/admin.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 68 | -------------------------------------------------------------------------------- /pages/admin/categories.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | -------------------------------------------------------------------------------- /pages/admin/notifications.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/admin/organizations.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/admin/pages.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/admin/settings.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/admin/tags.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | -------------------------------------------------------------------------------- /pages/admin/users.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/logout.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 47 | -------------------------------------------------------------------------------- /pages/moderation.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 37 | -------------------------------------------------------------------------------- /pages/post/_slug.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 55 | -------------------------------------------------------------------------------- /pages/post/_slug/take-action.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/post/create.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /pages/post/edit/_id.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 82 | -------------------------------------------------------------------------------- /pages/profile/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /pages/settings.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 63 | -------------------------------------------------------------------------------- /pages/settings/data-download.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/settings/delete-account.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/settings/invites.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/settings/languages.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/settings/my-organizations.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/settings/security.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /plugins/apollo-config.js: -------------------------------------------------------------------------------- 1 | export default ({ app }) => { 2 | const backendUrl = process.env.BACKEND_URL || 'http://localhost:4000' 3 | return { 4 | httpEndpoint: process.server ? backendUrl : '/api', 5 | httpLinkOptions: { 6 | credentials: 'same-origin' 7 | }, 8 | credentials: true, 9 | tokenName: 'human-connection-token', 10 | persisting: false, 11 | websocketsOnly: false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugins/axios.js: -------------------------------------------------------------------------------- 1 | export default ({ $axios, app }) => { 2 | $axios.onRequest(config => { 3 | console.log(Object.keys(app)) 4 | // add current ui language 5 | config.headers['Accept-Language'] = app.$i18n.locale() 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import vuexI18n from 'vuex-i18n/dist/vuex-i18n.umd.js' 4 | import { debounce, isEmpty, find } from 'lodash' 5 | 6 | /** 7 | * TODO: Refactor and simplify browser detection 8 | * and implement the user preference logic 9 | */ 10 | export default ({ app, req, cookie, store }) => { 11 | const debug = app.$env.NODE_ENV !== 'production' 12 | const key = 'locale' 13 | 14 | const changeHandler = async mutation => { 15 | if (process.server) return 16 | 17 | const newLocale = mutation.payload.locale 18 | const currentLocale = await app.$cookies.get(key) 19 | const isDifferent = newLocale !== currentLocale 20 | 21 | if (!isDifferent) { 22 | return 23 | } 24 | 25 | app.$cookies.set(key, newLocale) 26 | if (!app.$i18n.localeExists(newLocale)) { 27 | import(`~/locales/${newLocale}.json`).then(res => { 28 | app.$i18n.add(newLocale, res.default) 29 | }) 30 | } 31 | 32 | const user = store.getters['auth/user'] 33 | const token = store.getters['auth/token'] 34 | // persist language if it differs from last value 35 | if (user && user._id && token) { 36 | // TODO: SAVE LOCALE 37 | // store.dispatch('usersettings/patch', { 38 | // uiLanguage: newLocale 39 | // }, { root: true }) 40 | } 41 | } 42 | 43 | // const i18nStore = new Vuex.Store({ 44 | // strict: debug 45 | // }) 46 | 47 | Vue.use(vuexI18n.plugin, store, { 48 | onTranslationNotFound: function(locale, key) { 49 | if (debug) { 50 | console.warn( 51 | `vuex-i18n :: Key '${key}' not found for locale '${locale}'` 52 | ) 53 | } 54 | } 55 | }) 56 | 57 | // register the fallback locales 58 | Vue.i18n.add('en', require('~/locales/en.json')) 59 | 60 | let userLocale = 'en' 61 | const localeCookie = app.$cookies.get(key) 62 | /* const userSettings = store.getters['auth/userSettings'] 63 | if (userSettings && userSettings.uiLanguage) { 64 | // try to get saved user preference 65 | userLocale = userSettings.uiLanguage 66 | } else */ 67 | if (!isEmpty(localeCookie)) { 68 | userLocale = localeCookie 69 | } else { 70 | try { 71 | userLocale = process.browser 72 | ? navigator.language || navigator.userLanguage 73 | : req.headers['accept-language'].split(',')[0] 74 | } catch (err) {} 75 | 76 | if (userLocale && !isEmpty(userLocale.language)) { 77 | userLocale = userLocale.language.substr(0, 2) 78 | } 79 | } 80 | 81 | const availableLocales = process.env.locales.filter(lang => !!lang.enabled) 82 | const locale = find(availableLocales, ['code', userLocale]) 83 | ? userLocale 84 | : 'en' 85 | 86 | if (locale !== 'en') { 87 | Vue.i18n.add(locale, require(`~/locales/${locale}.json`)) 88 | } 89 | 90 | // Set the start locale to use 91 | Vue.i18n.set(locale) 92 | Vue.i18n.fallback('en') 93 | 94 | if (process.browser) { 95 | store.subscribe(mutation => { 96 | if (mutation.type === 'i18n/SET_LOCALE') { 97 | changeHandler(mutation) 98 | } 99 | }) 100 | } 101 | 102 | app.$i18n = Vue.i18n 103 | 104 | return store 105 | } 106 | -------------------------------------------------------------------------------- /plugins/izi-toast.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueIziToast from 'vue-izitoast' 3 | 4 | import 'izitoast/dist/css/iziToast.css' 5 | 6 | export default ({ app }) => { 7 | Vue.use(VueIziToast, { 8 | position: 'bottomRight', 9 | transitionIn: 'bounceInLeft', 10 | layout: 2, 11 | theme: 'dark' 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /plugins/keep-alive.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | let lastRoute 4 | const keepAliveHook = {} 5 | 6 | if (!process.server) { 7 | keepAliveHook.install = Vue => { 8 | const keepAlivePages = process.env.keepAlivePages || [] 9 | 10 | Vue.mixin({ 11 | // Save route if this instance is a page (has metaInfo) 12 | mounted() { 13 | if (this.$metaInfo) { 14 | lastRoute = this.$route.name 15 | } 16 | }, 17 | activated() { 18 | if (this.$metaInfo) { 19 | lastRoute = this.$route.name 20 | } 21 | }, 22 | deactivated() { 23 | // If this is a page and we don't want it to be kept alive 24 | if (this.$metaInfo && !keepAlivePages.includes(lastRoute)) { 25 | this.$destroy() 26 | } 27 | } 28 | }) 29 | } 30 | Vue.use(keepAliveHook) 31 | } 32 | -------------------------------------------------------------------------------- /plugins/styleguide-dev.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Styleguide from '@@' 3 | 4 | Vue.use(Styleguide) 5 | -------------------------------------------------------------------------------- /plugins/styleguide.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Styleguide from '@human-connection/styleguide' 3 | import '@human-connection/styleguide/dist/system.css' 4 | 5 | Vue.use(Styleguide) 6 | -------------------------------------------------------------------------------- /plugins/v-tooltip.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VTooltip from 'v-tooltip' 3 | 4 | Vue.use(VTooltip, { 5 | defaultDelay: { 6 | show: 500, 7 | hide: 50 8 | }, 9 | defaultOffset: 2, 10 | defaultPopperOptions: { 11 | removeOnDestroy: true 12 | }, 13 | popover: { 14 | // defaultArrowClass: 'm-dropdown__arrow' 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /plugins/vue-directives.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default ({ app }) => { 4 | Vue.directive('focus', { 5 | // When the bound element is inserted into the DOM... 6 | inserted: (el, binding) => { 7 | // Focus the element 8 | Vue.nextTick(() => { 9 | if (binding.value !== false) { 10 | el.focus() 11 | } 12 | }) 13 | } 14 | }) 15 | 16 | Vue.directive('router-link', { 17 | bind: (el, binding) => { 18 | binding.clickEventListener = e => { 19 | if (!e.metaKey && !e.ctrlKey) { 20 | e.preventDefault() 21 | app.router.push(el.getAttribute('href')) 22 | } 23 | } 24 | el.addEventListener('click', binding.clickEventListener) 25 | }, 26 | unbind: (el, binding) => { 27 | // cleanup 28 | if (binding.clickEventListener) { 29 | el.removeEventListener('click', binding.clickEventListener) 30 | } 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /plugins/vue-filters.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import { enUS, de, nl, fr, es } from 'date-fns/locale' 4 | import format from 'date-fns/format' 5 | import formatRelative from 'date-fns/formatRelative' 6 | import addSeconds from 'date-fns/addSeconds' 7 | import accounting from 'accounting' 8 | 9 | export default ({ app }) => { 10 | const locales = { 11 | en: enUS, 12 | de: de, 13 | nl: nl, 14 | fr: fr, 15 | es: es, 16 | pt: es, 17 | pl: de 18 | } 19 | const getLocalizedFormat = () => { 20 | let locale = app.$i18n.locale() 21 | locale = locales[locale] ? locale : 'en' 22 | return locales[locale] 23 | } 24 | app.$filters = Object.assign(app.$filters || {}, { 25 | date: (value, fmt = 'dd. MMM yyyy') => { 26 | if (!value) return '' 27 | return format(new Date(value), fmt, { 28 | locale: getLocalizedFormat() 29 | }) 30 | }, 31 | dateTime: (value, fmt = 'dd. MMM yyyy HH:mm') => { 32 | if (!value) return '' 33 | return format(new Date(value), fmt, { 34 | locale: getLocalizedFormat() 35 | }) 36 | }, 37 | relativeDateTime: value => { 38 | if (!value) return '' 39 | return formatRelative(new Date(value), new Date(), { 40 | locale: getLocalizedFormat() 41 | }) 42 | }, 43 | number: ( 44 | value, 45 | precision = 2, 46 | thousands = '.', 47 | decimals = ',', 48 | fallback = null 49 | ) => { 50 | if (isNaN(value) && fallback) { 51 | return fallback 52 | } 53 | return accounting.formatNumber(value || 0, precision, thousands, decimals) 54 | }, 55 | // format seconds or milliseconds to durations HH:mm:ss 56 | duration: (value, unit = 's') => { 57 | if (unit === 'ms') { 58 | value = value / 1000 59 | } 60 | return value 61 | ? format(addSeconds(new Date('2000-01-01 00:00'), value), 'HH:mm:ss') 62 | : '00:00:00' 63 | }, 64 | truncate: (value = '', length = -1) => { 65 | if (!value || typeof value !== 'string' || value.length <= 0) { 66 | return '' 67 | } 68 | if (length <= 0) { 69 | return value 70 | } 71 | let output = value.substring(0, length) 72 | if (output.length < value.length) { 73 | output += '…' 74 | } 75 | return output 76 | }, 77 | list: (value, glue = ', ', truncate = 0) => { 78 | if (!Array.isArray(value) || !value.length) { 79 | return '' 80 | } 81 | if (truncate > 0) { 82 | value = value.map(item => { 83 | return app.$filters.truncate(item, truncate) 84 | }) 85 | } 86 | return value.join(glue) 87 | }, 88 | listByKey: (values, key, glue, truncate) => { 89 | return app.$filters.list(values.map(item => item[key]), glue, truncate) 90 | }, 91 | camelCase: (value = '') => { 92 | return value 93 | .replace(/(?:^\w|[A-Za-z]|\b\w)/g, (letter, index) => { 94 | return index === 0 ? letter.toUpperCase() : letter.toLowerCase() 95 | }) 96 | .replace(/\s+/g, '') 97 | } 98 | }) 99 | 100 | // add all methods as filters on each vue component 101 | Object.keys(app.$filters).forEach(key => { 102 | Vue.filter(key, app.$filters[key]) 103 | }) 104 | 105 | Vue.prototype.$filters = app.$filters 106 | 107 | return app 108 | } 109 | -------------------------------------------------------------------------------- /screenshot-styleguide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/screenshot-styleguide.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/screenshot.png -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "See me deployed at $1 :)" 4 | 5 | -------------------------------------------------------------------------------- /scripts/docker_push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 3 | docker push humanconnection/nitro-web:latest 4 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const consola = require('consola') 3 | const { Nuxt, Builder } = require('nuxt') 4 | const app = express() 5 | 6 | require('dotenv').config() 7 | 8 | const host = process.env.HOST || '127.0.0.1' 9 | const port = process.env.PORT || 3000 10 | app.set('port', port) 11 | 12 | // Import and Set Nuxt.js options 13 | let config = require('../nuxt.config.js') 14 | config.dev = !(process.env.NODE_ENV === 'production') 15 | 16 | async function start() { 17 | // Init Nuxt.js 18 | const nuxt = new Nuxt(config) 19 | 20 | // Build only in dev mode 21 | if (config.dev) { 22 | const builder = new Builder(nuxt) 23 | await builder.build() 24 | } 25 | 26 | // Give nuxt middleware to express 27 | app.use(nuxt.render) 28 | 29 | // Listen the server 30 | app.listen(port, host) 31 | consola.ready({ 32 | message: `Server listening on http://${host}:${port}`, 33 | badge: true 34 | }) 35 | } 36 | start() 37 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | 8 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 11 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/static/favicon.ico -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_airship.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_alienship.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_balloon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_bigballoon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_crane.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_glider.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_helicopter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/fundraisingbox_de_starter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/indiegogo_en_bear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/indiegogo_en_panda.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/indiegogo_en_rabbit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/indiegogo_en_rhino.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/indiegogo_en_tiger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/indiegogo_en_whale.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/indiegogo_en_wolf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/user_role_developer.svg: -------------------------------------------------------------------------------- 1 | </> -------------------------------------------------------------------------------- /static/img/badges/user_role_moderator.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/wooold_de_bee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/wooold_de_butterfly.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/wooold_de_double_rainbow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/wooold_de_flower.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/badges/wooold_de_magic_rainbow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/empty-state.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/empty/alert.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/empty/docs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/empty/events.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/empty/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/empty/messages.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/empty/tasks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/de.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/en.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/es.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/fr.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/it.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/nl.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/pl.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/locale-flags/pt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/sign-up/alpha-invite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/static/img/sign-up/alpha-invite.png -------------------------------------------------------------------------------- /static/img/sign-up/alpha-invite2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/static/img/sign-up/alpha-invite2x.png -------------------------------------------------------------------------------- /static/img/sign-up/humanconnection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/static/img/sign-up/humanconnection.png -------------------------------------------------------------------------------- /static/img/sign-up/nicetomeetyou.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/static/img/sign-up/nicetomeetyou.png -------------------------------------------------------------------------------- /static/img/sign-up/onourjourney.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Human-Connection/Nitro-Web/97f998b0c3033607783ab81f8f6fb68ae10c21f6/static/img/sign-up/onourjourney.png -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory activate the option in the framework automatically. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /store/auth.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | import jwt from 'jsonwebtoken' 3 | 4 | export const state = () => { 5 | return { 6 | user: null, 7 | token: null, 8 | pending: false 9 | } 10 | } 11 | 12 | export const mutations = { 13 | SET_USER(state, user) { 14 | state.user = user || null 15 | }, 16 | SET_TOKEN(state, token) { 17 | state.token = token || null 18 | }, 19 | SET_PENDING(state, pending) { 20 | state.pending = pending 21 | } 22 | } 23 | 24 | export const getters = { 25 | isAuthenticated(state) { 26 | return !!state.token 27 | }, 28 | isLoggedIn(state) { 29 | return !!(state.user && state.token) 30 | }, 31 | pending(state) { 32 | return !!state.pending 33 | }, 34 | isAdmin(state) { 35 | return !!state.user && state.user.role === 'admin' 36 | }, 37 | isModerator(state) { 38 | return ( 39 | !!state.user && 40 | (state.user.role === 'admin' || state.user.role === 'moderator') 41 | ) 42 | }, 43 | user(state) { 44 | return state.user || {} 45 | }, 46 | token(state) { 47 | return state.token 48 | } 49 | } 50 | 51 | export const actions = { 52 | async init({ commit, dispatch }) { 53 | if (!process.server) { 54 | return 55 | } 56 | const token = this.app.$apolloHelpers.getToken() 57 | if (!token) { 58 | return 59 | } 60 | commit('SET_TOKEN', token) 61 | await dispatch('fetchCurrentUser') 62 | }, 63 | 64 | async check({ commit, dispatch, getters }) { 65 | if (!this.app.$apolloHelpers.getToken()) { 66 | await dispatch('logout') 67 | } 68 | return getters.isLoggedIn 69 | }, 70 | 71 | async fetchCurrentUser({ commit, dispatch }) { 72 | const client = this.app.apolloProvider.defaultClient 73 | const { 74 | data: { currentUser } 75 | } = await client.query({ 76 | query: gql(`{ 77 | currentUser { 78 | id 79 | name 80 | slug 81 | email 82 | avatar 83 | role 84 | about 85 | locationName 86 | } 87 | }`) 88 | }) 89 | if (!currentUser) return dispatch('logout') 90 | commit('SET_USER', currentUser) 91 | return currentUser 92 | }, 93 | 94 | async login({ commit, dispatch }, { email, password }) { 95 | commit('SET_PENDING', true) 96 | try { 97 | const client = this.app.apolloProvider.defaultClient 98 | const { 99 | data: { login } 100 | } = await client.mutate({ 101 | mutation: gql(` 102 | mutation($email: String!, $password: String!) { 103 | login(email: $email, password: $password) 104 | } 105 | `), 106 | variables: { email, password } 107 | }) 108 | await this.app.$apolloHelpers.onLogin(login) 109 | commit('SET_TOKEN', login) 110 | await dispatch('fetchCurrentUser') 111 | } catch (err) { 112 | throw new Error(err) 113 | } finally { 114 | commit('SET_PENDING', false) 115 | } 116 | }, 117 | 118 | async logout({ commit }) { 119 | commit('SET_USER', null) 120 | commit('SET_TOKEN', null) 121 | return this.app.$apolloHelpers.onLogout() 122 | }, 123 | 124 | register( 125 | { dispatch, commit }, 126 | { email, password, inviteCode, invitedByUserId } 127 | ) {}, 128 | async patch({ state, commit, dispatch }, data) {}, 129 | resendVerifySignup({ state, dispatch }) {}, 130 | resetPassword({ state }, data) {}, 131 | setNewPassword({ state }, data) {} 132 | } 133 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({}) 2 | 3 | export const mutations = {} 4 | 5 | export const actions = { 6 | async nuxtServerInit({ dispatch }) { 7 | await dispatch('auth/init') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /store/modal.js: -------------------------------------------------------------------------------- 1 | export const state = () => { 2 | return { 3 | open: null, 4 | data: {} 5 | } 6 | } 7 | 8 | export const mutations = { 9 | SET_OPEN(state, ctx) { 10 | state.open = ctx.name || null 11 | state.data = ctx.data || {} 12 | } 13 | } 14 | 15 | export const getters = { 16 | open(state) { 17 | return state.open 18 | }, 19 | data(state) { 20 | return state.data 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /store/search.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | import isString from 'lodash/isString' 3 | 4 | export const state = () => { 5 | return { 6 | quickResults: [], 7 | quickPending: false, 8 | quickValue: '' 9 | } 10 | } 11 | 12 | export const mutations = { 13 | SET_QUICK_RESULTS(state, results) { 14 | state.quickResults = results || [] 15 | state.quickPending = false 16 | }, 17 | SET_QUICK_PENDING(state, pending) { 18 | state.quickPending = pending 19 | }, 20 | SET_QUICK_VALUE(state, value) { 21 | state.quickValue = value 22 | } 23 | } 24 | 25 | export const getters = { 26 | quickResults(state) { 27 | return state.quickResults 28 | }, 29 | quickPending(state) { 30 | return state.quickPending 31 | }, 32 | quickValue(state) { 33 | return state.quickValue 34 | } 35 | } 36 | 37 | export const actions = { 38 | async quickSearch({ commit, getters }, { value }) { 39 | value = isString(value) ? value.trim() : '' 40 | const lastVal = getters.quickValue 41 | if (value.length < 3 || lastVal.toLowerCase() === value.toLowerCase()) { 42 | return 43 | } 44 | commit('SET_QUICK_VALUE', value) 45 | commit('SET_QUICK_PENDING', true) 46 | await this.app.apolloProvider.defaultClient 47 | .query({ 48 | query: gql(` 49 | query findPosts($filter: String!) { 50 | findPosts(filter: $filter, limit: 10) { 51 | id 52 | slug 53 | label: title 54 | value: title, 55 | shoutedCount 56 | commentsCount 57 | createdAt 58 | author { 59 | id 60 | name 61 | slug 62 | } 63 | } 64 | } 65 | `), 66 | variables: { 67 | filter: value.replace(/\s/g, '~ ') + '~' 68 | } 69 | }) 70 | .then(res => { 71 | commit('SET_QUICK_RESULTS', res.data.findPosts || []) 72 | }) 73 | .catch(() => { 74 | commit('SET_QUICK_RESULTS', []) 75 | }) 76 | .finally(() => { 77 | commit('SET_QUICK_PENDING', false) 78 | }) 79 | return getters.quickResults 80 | }, 81 | async quickClear({ commit }) { 82 | commit('SET_QUICK_PENDING', false) 83 | commit('SET_QUICK_RESULTS', []) 84 | commit('SET_QUICK_VALUE', '') 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /styleguide/src/system/icons/svg/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | bold 4 | 5 | 6 | -------------------------------------------------------------------------------- /styleguide/src/system/icons/svg/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | italic 4 | 5 | 6 | -------------------------------------------------------------------------------- /styleguide/src/system/icons/svg/list-ol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | list-ol 4 | 5 | 6 | -------------------------------------------------------------------------------- /styleguide/src/system/icons/svg/list-ul.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | list-ul 4 | 5 | 6 | -------------------------------------------------------------------------------- /styleguide/src/system/icons/svg/paragraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | paragraph 4 | 5 | 6 | -------------------------------------------------------------------------------- /styleguide/src/system/icons/svg/quote-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | quote-right 4 | 5 | 6 | --------------------------------------------------------------------------------