├── .coveralls.yml ├── .modernizrrc ├── assets ├── style │ ├── _confetti.scss │ ├── _overflows.scss │ ├── _buttons.scss │ ├── _fonts.scss │ ├── _code.scss │ ├── _tabs.scss │ ├── _labels.scss │ ├── _badges.scss │ ├── _scrollbar.scss │ ├── _modals.scss │ ├── _transitions.scss │ ├── _cards.scss │ ├── _tables.scss │ ├── _charts.scss │ ├── _forms.scss │ └── _images.scss └── img │ ├── code.png │ ├── code.webp │ ├── newton.jpg │ ├── wreath.png │ ├── dark-wall.png │ ├── geometry.png │ ├── geometry.webp │ ├── newton.webp │ ├── wreath.webp │ ├── dark-wall.webp │ ├── white-wall.png │ ├── white-wall.webp │ ├── app-background.jpg │ ├── geometry-dark.png │ ├── geometry-dark.webp │ ├── site-homepage.png │ ├── site-homepage.webp │ └── app-background.webp ├── static └── icon.png ├── plugins ├── vue-moment.js ├── nuxt-client-init.js ├── vue-scrollto.js ├── vue-isotope.js ├── vue-simplemde.js ├── vue-sweetalert.js ├── libcrowds-viewer.js ├── vue-awesome.js ├── vue-gravatar.js ├── vue-toggle-button.js ├── modernizr.js ├── vue-confetti.js ├── vue-multiselect.js ├── vue-images-loaded.js ├── vue-chartist.js ├── vue-clickaway.js ├── vue-infinite-loading.js ├── vue-prevent-parent-scroll.js ├── dark-mode.js ├── vue-form-generator.js ├── filters.js ├── cookie-consent.js ├── axios.js ├── cookies.js └── notifications.js ├── test ├── .eslintrc.js ├── unit │ └── specs │ │ ├── components │ │ ├── charts │ │ │ ├── __snapshots__ │ │ │ │ ├── bar.spec.js.snap │ │ │ │ ├── line.spec.js.snap │ │ │ │ └── pie.spec.js.snap │ │ │ ├── bar.spec.js │ │ │ ├── line.spec.js │ │ │ └── pie.spec.js │ │ ├── buttons │ │ │ ├── __snapshots__ │ │ │ │ ├── projectContrib.spec.js.snap │ │ │ │ ├── oauth.spec.js.snap │ │ │ │ └── socialMedia.spec.js.snap │ │ │ ├── projectContrib.spec.js │ │ │ ├── oauth.spec.js │ │ │ └── socialMedia.spec.js │ │ ├── footers │ │ │ ├── dashboard.spec.js │ │ │ └── app.spec.js │ │ └── cards │ │ │ ├── profile.spec.js │ │ │ ├── collection.spec.js │ │ │ └── __snapshots__ │ │ │ ├── collection.spec.js.snap │ │ │ └── profile.spec.js.snap │ │ ├── pages │ │ ├── collection │ │ │ ├── __snapshots__ │ │ │ │ └── about.spec.js.snap │ │ │ └── about.spec.js │ │ └── help │ │ │ ├── api.spec.js │ │ │ ├── tos.spec.js │ │ │ ├── cookies.spec.js │ │ │ ├── privacy.spec.js │ │ │ └── __snapshots__ │ │ │ ├── api.spec.js.snap │ │ │ └── cookies.spec.js.snap │ │ └── utils │ │ └── auth.spec.js ├── assetsTransformer.js ├── fixtures │ ├── collection.json │ └── project.json └── test.local.config.js ├── backpack.config.js ├── mixins ├── computeShareUrl.js ├── getShortname.js ├── fetchCollectionByName.js ├── handleHashedFlashes.js ├── fetchProjectByName.js ├── exportFile.js ├── fetchProjectAndCollection.js ├── hideCookieConsent.js ├── deleteDomainObject.js ├── fetchCollectionAndTmpl.js ├── licenses.js └── currentMicrositeNavItems.js ├── store ├── mutations.js └── index.js ├── .editorconfig ├── .gitignore ├── components ├── charts │ ├── Legend.vue │ ├── Pie.vue │ ├── Bar.vue │ └── Line.vue ├── buttons │ ├── ProjectContrib.vue │ └── Clipboard.vue ├── avatars │ ├── Base.vue │ ├── Project.vue │ └── User.vue ├── data │ ├── DownloadAnnotationData.vue │ ├── FilterProjects.vue │ └── DownloadProjectData.vue ├── lists │ ├── ProjectFilters.vue │ └── ItemTags.vue ├── modals │ ├── Leaderboard.vue │ └── AddProjectFilter.vue ├── cards │ ├── Base.vue │ └── Profile.vue ├── forms │ ├── Base.vue │ ├── Modal.vue │ └── fields │ │ └── Array.vue └── footers │ └── Dashboard.vue ├── middleware ├── is-logged-in.js ├── session.js ├── is-admin.js ├── is-current-or-admin.js └── project-management.js ├── layouts ├── default.vue ├── admin-site-dashboard.vue ├── container.vue ├── collection-fullscreen-dark.vue ├── collection-default.vue ├── collection-tabs.vue ├── help-dashboard.vue ├── error.vue ├── account-dashboard.vue └── bases │ └── Dashboard.vue ├── .travis.yml ├── bin └── convertToWebp.js ├── .babelrc ├── utils ├── getDefaultEmail.js ├── batch.js ├── auth.js └── fetchAll.js ├── modules └── nuxt-explicates │ └── module.js ├── pages ├── account │ ├── signout.vue │ ├── register │ │ └── confirmation.vue │ ├── forgot-password.vue │ ├── _name │ │ ├── settings │ │ │ ├── preferences.vue │ │ │ ├── api.vue │ │ │ ├── avatar.vue │ │ │ └── security.vue │ │ ├── announcements.vue │ │ └── index.vue │ ├── newsletter.vue │ └── reset-password.vue ├── admin │ ├── site │ │ ├── jobs.vue │ │ └── announcements │ │ │ └── index.vue │ ├── collection │ │ ├── index.vue │ │ ├── _short_name │ │ │ └── delete.vue │ │ └── new.vue │ ├── template │ │ ├── index.vue │ │ └── _short_name │ │ │ ├── index.vue │ │ │ └── _id │ │ │ ├── tutorial.vue │ │ │ ├── index.vue │ │ │ └── parent.vue │ └── project │ │ ├── _short_name │ │ ├── webhooks.vue │ │ ├── delete.vue │ │ ├── thumbnail.vue │ │ └── volume.vue │ │ └── new │ │ └── index.vue ├── collection │ └── _short_name │ │ ├── projects │ │ └── _id │ │ │ └── index.vue │ │ └── about.vue └── help │ ├── api.vue │ └── cookies.vue ├── .eslintrc.js ├── server └── index.js ├── README.md ├── LICENSE └── local.config.js.tmpl /.coveralls.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.modernizrrc: -------------------------------------------------------------------------------- 1 | { 2 | "feature-detects": [ 3 | "img/webp" 4 | ] 5 | } -------------------------------------------------------------------------------- /assets/style/_confetti.scss: -------------------------------------------------------------------------------- 1 | #confetti-canvas { 2 | z-index: 2000; 3 | } 4 | -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/static/icon.png -------------------------------------------------------------------------------- /assets/img/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/code.png -------------------------------------------------------------------------------- /assets/style/_overflows.scss: -------------------------------------------------------------------------------- 1 | .overflow-visible { 2 | overflow: visible !important; 3 | } -------------------------------------------------------------------------------- /assets/img/code.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/code.webp -------------------------------------------------------------------------------- /assets/img/newton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/newton.jpg -------------------------------------------------------------------------------- /assets/img/wreath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/wreath.png -------------------------------------------------------------------------------- /assets/style/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn-dark { 2 | @include button-variant($gray-1200, $gray-1200); 3 | } -------------------------------------------------------------------------------- /assets/img/dark-wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/dark-wall.png -------------------------------------------------------------------------------- /assets/img/geometry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/geometry.png -------------------------------------------------------------------------------- /assets/img/geometry.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/geometry.webp -------------------------------------------------------------------------------- /assets/img/newton.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/newton.webp -------------------------------------------------------------------------------- /assets/img/wreath.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/wreath.webp -------------------------------------------------------------------------------- /assets/img/dark-wall.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/dark-wall.webp -------------------------------------------------------------------------------- /assets/img/white-wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/white-wall.png -------------------------------------------------------------------------------- /assets/img/white-wall.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/white-wall.webp -------------------------------------------------------------------------------- /assets/img/app-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/app-background.jpg -------------------------------------------------------------------------------- /assets/img/geometry-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/geometry-dark.png -------------------------------------------------------------------------------- /assets/img/geometry-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/geometry-dark.webp -------------------------------------------------------------------------------- /assets/img/site-homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/site-homepage.png -------------------------------------------------------------------------------- /assets/img/site-homepage.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/site-homepage.webp -------------------------------------------------------------------------------- /assets/img/app-background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibCrowds/libcrowds/HEAD/assets/img/app-background.webp -------------------------------------------------------------------------------- /plugins/vue-moment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueMoment from 'vue-moment' 3 | 4 | Vue.use(VueMoment) 5 | -------------------------------------------------------------------------------- /plugins/nuxt-client-init.js: -------------------------------------------------------------------------------- 1 | export default async (ctx) => { 2 | await ctx.store.dispatch('nuxtClientInit', ctx) 3 | } 4 | -------------------------------------------------------------------------------- /plugins/vue-scrollto.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueScrollTo from 'vue-scrollto' 3 | 4 | Vue.use(VueScrollTo) 5 | -------------------------------------------------------------------------------- /plugins/vue-isotope.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import isotope from 'vueisotope' 3 | 4 | Vue.component('isotope', isotope) 5 | -------------------------------------------------------------------------------- /plugins/vue-simplemde.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueSimplemde from 'vue-simplemde' 3 | 4 | Vue.use(VueSimplemde) 5 | -------------------------------------------------------------------------------- /plugins/vue-sweetalert.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueSweetAlert from 'vue-sweetalert' 3 | 4 | Vue.use(VueSweetAlert) 5 | -------------------------------------------------------------------------------- /plugins/libcrowds-viewer.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import LibcrowdsViewer from 'libcrowds-viewer' 3 | 4 | Vue.use(LibcrowdsViewer) 5 | -------------------------------------------------------------------------------- /plugins/vue-awesome.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Icon from 'vue-awesome/components/Icon' 3 | 4 | Vue.component('icon', Icon) 5 | -------------------------------------------------------------------------------- /plugins/vue-gravatar.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueGravatar from 'vue-gravatar' 3 | 4 | Vue.component('v-gravatar', VueGravatar) 5 | -------------------------------------------------------------------------------- /plugins/vue-toggle-button.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueToggleButton from 'vue-js-toggle-button' 3 | 4 | Vue.use(VueToggleButton) 5 | -------------------------------------------------------------------------------- /plugins/modernizr.js: -------------------------------------------------------------------------------- 1 | import Modernizr from 'modernizr' 2 | 3 | export default (ctx, inject) => { 4 | inject('modernizr', Modernizr) 5 | } 6 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // allow unhandled errors in tests 4 | 'handle-callback-err': 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /plugins/vue-confetti.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueConfetti from 'vue-confetti' 3 | 4 | Vue.use(VueConfetti, { 5 | shape: 'heart' 6 | }) 7 | -------------------------------------------------------------------------------- /plugins/vue-multiselect.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Multiselect from 'vue-multiselect' 3 | 4 | Vue.component('multiselect', Multiselect) 5 | -------------------------------------------------------------------------------- /plugins/vue-images-loaded.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import imagesLoaded from 'vue-images-loaded' 3 | 4 | Vue.directive('images-loaded', imagesLoaded) 5 | -------------------------------------------------------------------------------- /plugins/vue-chartist.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueChartist from 'vue-chartist' 3 | import 'chartist-plugin-tooltips' 4 | 5 | Vue.use(VueChartist) 6 | -------------------------------------------------------------------------------- /plugins/vue-clickaway.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { directive as onClickaway } from 'vue-clickaway' 3 | 4 | Vue.directive('on-clickaway', onClickaway) 5 | -------------------------------------------------------------------------------- /backpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: (config, options, webpack) => { 3 | config.entry.main = './server/index.js' 4 | return config 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /plugins/vue-infinite-loading.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import InfiniteLoading from 'vue-infinite-loading' 3 | 4 | Vue.component('infinite-loading', InfiniteLoading) 5 | -------------------------------------------------------------------------------- /plugins/vue-prevent-parent-scroll.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VuePreventParentScroll from 'vue-prevent-parent-scroll' 3 | 4 | Vue.use(VuePreventParentScroll) 5 | -------------------------------------------------------------------------------- /assets/style/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Varela+Round:300,400,700); 2 | @import url(https://fonts.googleapis.com/css?family=Roboto:300,400,700); 3 | -------------------------------------------------------------------------------- /mixins/computeShareUrl.js: -------------------------------------------------------------------------------- 1 | export const computeShareUrl = { 2 | computed: { 3 | shareUrl () { 4 | return process.browser ? window.location.href : '' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /plugins/dark-mode.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.mixin({ 4 | computed: { 5 | darkMode () { 6 | return this.$store.state.darkMode 7 | } 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /store/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_ITEM: (state, obj) => { 3 | state[obj.key] = obj.value 4 | }, 5 | DELETE_ITEM: (state, key) => { 6 | state[key] = null 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /test/unit/specs/components/charts/__snapshots__/bar.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Bar chart renders correctly 1`] = `
`; 4 | -------------------------------------------------------------------------------- /test/unit/specs/components/charts/__snapshots__/line.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Line chart renders correctly 1`] = `
`; 4 | -------------------------------------------------------------------------------- /test/unit/specs/components/charts/__snapshots__/pie.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pie chart renders correctly 1`] = `
`; 4 | -------------------------------------------------------------------------------- /assets/style/_code.scss: -------------------------------------------------------------------------------- 1 | .markdown-editor{ 2 | .CodeMirror { 3 | height: 300px; 4 | } 5 | 6 | &[data-size="sm"] { 7 | .CodeMirror { 8 | height: 100px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/assetsTransformer.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | process (src, fn, config, options) { 3 | console.info('Assets transformer processing') 4 | return `module.exports = JSON.stringify(require('path').basename(fn));` 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /plugins/vue-form-generator.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueFormGenerator from 'vue-form-generator' 3 | import fieldArray from '@/components/forms/fields/Array.vue' 4 | 5 | Vue.use(VueFormGenerator) 6 | 7 | Vue.component('fieldArray', fieldArray) 8 | -------------------------------------------------------------------------------- /assets/style/_tabs.scss: -------------------------------------------------------------------------------- 1 | .dark-mode { 2 | .nav-tabs { 3 | .nav-item { 4 | .nav-link { 5 | background-color: transparent; 6 | border-bottom: none; 7 | 8 | &.active { 9 | color: $gray-100; 10 | border-color: $gray-800; 11 | } 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /assets/style/_labels.scss: -------------------------------------------------------------------------------- 1 | label { 2 | &.label-muted { 3 | font-weight: 300; 4 | font-size: $font-size-sm; 5 | color: $gray-600; 6 | margin: 1.25rem 0 .625rem; 7 | } 8 | 9 | &.toggle-label { 10 | font-family: $font-family-base; 11 | font-weight: 400; 12 | font-size: $font-size-sm; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/style/_badges.scss: -------------------------------------------------------------------------------- 1 | .badge-list { 2 | .badge { 3 | letter-spacing: 0.2px; 4 | font-weight: 400; 5 | font-size: 90%; 6 | margin-left: 0.25rem; 7 | margin-right: 0.25rem; 8 | 9 | &:first-child { 10 | margin-left: 0; 11 | } 12 | 13 | &:last-child { 14 | margin-right: 0; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | 13 | # Backpack build 14 | build 15 | 16 | # Coverage 17 | test/unit/coverage 18 | 19 | # Config 20 | local.config.js 21 | 22 | # PWA 23 | sw.* 24 | workbox-* 25 | 26 | # Vagrant 27 | .vagrant/ 28 | nodesource_setup.sh 29 | -------------------------------------------------------------------------------- /components/charts/Legend.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /middleware/is-logged-in.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty' 2 | 3 | /** 4 | * Middleware to check if the user is logged in. 5 | * @param {Object} context 6 | * The nuxt context. 7 | */ 8 | export default function ({ store, route, redirect }) { 9 | const currentUser = store.state.currentUser 10 | if (isEmpty(currentUser)) { 11 | redirect(`/account/signin?next=${route.path}`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "short_name": "playbills", 4 | "name": "In the Spotlight", 5 | "description": "Help rediscover popular entertainment.", 6 | "info": { 7 | "tagline": "Bring past performances to life.", 8 | "forum": "http://example.com/discuss", 9 | "background": "http://example.com/background.jpg", 10 | "presenter": "libcrowds-viewer" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /middleware/session.js: -------------------------------------------------------------------------------- 1 | import { updateSession } from '@/utils/auth' 2 | 3 | /** 4 | * Router middleware to manage the user's session. 5 | * @param {Object} context 6 | * The nuxt context. 7 | */ 8 | export default function ({ store, app }) { 9 | const currentUser = store.state.currentUser 10 | if (process.client && updateSession(currentUser, document.cookie)) { 11 | store.dispatch('UPDATE_CURRENT_USER', app.$axios, app.$ga) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/style/_scrollbar.scss: -------------------------------------------------------------------------------- 1 | .custom-scrollbar, 2 | .modal-body { 3 | &::-webkit-scrollbar { 4 | width: 0.5rem; 5 | } 6 | 7 | &::-webkit-scrollbar-thumb { 8 | background-color: $gray-400; 9 | outline: 1px solid $gray-200; 10 | } 11 | } 12 | 13 | .dark-mode { 14 | .custom-scrollbar::-webkit-scrollbar-thumb, 15 | .modal-body::-webkit-scrollbar-thumb { 16 | background-color: $gray-800; 17 | outline: 1px solid $gray-600; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - '8' 6 | - '9' 7 | - '10' 8 | before_install: 9 | # Remove the line below once node 9 moves to LTS (and npm 5 is standard) 10 | - npm install -g npm@5 11 | install: 12 | - npm install 13 | script: 14 | - cp local.config.js.tmpl local.config.js 15 | - npm run build 16 | - npm run test 17 | after_success: 18 | - npm install coveralls 19 | - cat ./test/unit/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js -------------------------------------------------------------------------------- /bin/convertToWebp.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const imagemin = require('imagemin') 3 | const webp = require('imagemin-webp') 4 | const outputFolder = path.resolve('assets/img') 5 | const PNGImages = path.resolve('assets/img/*.png') 6 | const JPEGImages = path.resolve('assets/img/*.jpg') 7 | 8 | imagemin([PNGImages], outputFolder, { 9 | plugins: [webp({ 10 | lossless: true 11 | })] 12 | }) 13 | 14 | imagemin([JPEGImages], outputFolder, { 15 | plugins: [webp({ 16 | quality: 75 17 | })] 18 | }) 19 | -------------------------------------------------------------------------------- /test/unit/specs/components/buttons/__snapshots__/projectContrib.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ProjectContrib button is active by default 1`] = ` 4 | 5 | Contribute 6 | 7 | `; 8 | 9 | exports[`ProjectContrib button is disabled when project is completed 1`] = ` 10 | 11 | Contribute 12 | 13 | `; 14 | -------------------------------------------------------------------------------- /plugins/filters.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import capitalize from 'capitalize' 3 | import pluralize from 'pluralize' 4 | 5 | Vue.filter('capitalize', capitalize) 6 | 7 | Vue.filter('pluralize', pluralize) 8 | 9 | Vue.filter('intComma', (n) => { 10 | if (n) { 11 | return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') 12 | } 13 | return 0 14 | }) 15 | 16 | Vue.filter('truncate', (str, n = 30) => { 17 | if (!str || str.length < n) { 18 | return str 19 | } 20 | return `${str.slice(0, n)}...` 21 | }) 22 | -------------------------------------------------------------------------------- /test/unit/specs/pages/collection/__snapshots__/about.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Collection about page renders correctly 1`] = ` 4 |
5 |

About undefined

About

6 |

Foo

7 |
8 |
9 |
10 |
11 | 12 | Get Started 13 | 14 |
15 |
16 | `; 17 | -------------------------------------------------------------------------------- /mixins/getShortname.js: -------------------------------------------------------------------------------- 1 | export const getShortname = { 2 | methods: { 3 | /** 4 | * Create a shortname by removing certain characters from a string. 5 | * @param {String} text 6 | * The text to convert. 7 | */ 8 | getShortname (text) { 9 | if (!text) { 10 | return '' 11 | } 12 | const badchars = /([$#%·:,.~!¡?"¿'=)(!&/|]+)/g 13 | const whitespace = /\s+/g 14 | return text.replace(badchars, '') 15 | .toLowerCase() 16 | .trim() 17 | .replace(whitespace, '_') 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/test.local.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | company: 'My Company', 3 | brand: 'My Brand', 4 | tagline: 'My inspiring tagline', 5 | description: 'My SEO optimised meta description', 6 | pybossaHost: 'http://example.com', 7 | libcrowdsHost: 'http://mylibcrowds.com', 8 | twitter: 'mytwitterhandle', 9 | email: 'me@example.com', 10 | github: 'https://github.com/Organization/repo', 11 | docs: 'http://docs.example.com', 12 | footer: { 13 | title: 'Newsletter', 14 | items: [ 15 | { text: 'Sign up', url: 'http://mailchimp.signup.url' } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /layouts/admin-site-dashboard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | -------------------------------------------------------------------------------- /mixins/fetchCollectionByName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch a collection by the short_name param and update the store. 3 | */ 4 | export const fetchCollectionByName = { 5 | fetch ({ params, app, error, store }) { 6 | return app.$axios.$get('/api/category', { 7 | params: { 8 | short_name: params.short_name 9 | } 10 | }).then(data => { 11 | if (!data || data.length !== 1) { 12 | error({ statusCode: 404 }) 13 | return 14 | } 15 | store.dispatch('UPDATE_CURRENT_COLLECTION', data[0]) 16 | }).catch(err => { 17 | error(err) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mixins/handleHashedFlashes.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string' 2 | 3 | /** 4 | * A mixin to handle hashed flash messages returned from PYBOSSA. 5 | */ 6 | export const handleHashedFlashes = { 7 | mounted () { 8 | if (process.browser) { 9 | const params = queryString.parse(location.search) 10 | const flash = params.flash 11 | if (flash) { 12 | const decodedFlash = decodeURIComponent(flash) 13 | const jsonStr = atob(decodedFlash) 14 | const json = JSON.parse(jsonStr) 15 | this.$notifications.flash(json) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mixins/fetchProjectByName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch a project by the short_name param and update the store. 3 | */ 4 | export const fetchProjectByName = { 5 | fetch ({ params, app, error, store }) { 6 | return app.$axios.$get('/api/project', { 7 | params: { 8 | short_name: params.short_name, 9 | all: 1 10 | } 11 | }).then(data => { 12 | if (!data || data.length !== 1) { 13 | error({ statusCode: 404 }) 14 | return 15 | } 16 | store.dispatch('UPDATE_CURRENT_PROJECT', data[0]) 17 | }).catch(err => { 18 | error(err) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /assets/style/_modals.scss: -------------------------------------------------------------------------------- 1 | .modal.show { 2 | display: block !important; 3 | 4 | .modal-dialog{ 5 | overflow-y: initial !important 6 | } 7 | 8 | .modal-header { 9 | display: inline-block; 10 | border-radius: 0; 11 | 12 | .modal-title { 13 | float: left; 14 | } 15 | } 16 | 17 | .modal-content { 18 | max-height: 85vh; 19 | border-radius: 0; 20 | } 21 | 22 | .modal-body { 23 | position: relative; 24 | max-height: 800px; 25 | overflow-y: auto; 26 | } 27 | 28 | .modal-footer { 29 | padding: 25px 15px; 30 | } 31 | 32 | img { 33 | max-width: 100%; 34 | } 35 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", { 5 | "modules": false, 6 | "targets": { 7 | "browsers": ["> 1%", "last 2 versions", "not ie <= 9"] 8 | } 9 | } 10 | ], 11 | "stage-2", 12 | "backpack-core/babel" 13 | ], 14 | "plugins": ["transform-runtime"], 15 | "env": { 16 | "testing": { 17 | "presets": [ 18 | [ 19 | "env", { 20 | "targets": { 21 | "node": "current" 22 | } 23 | } 24 | ], 25 | "stage-2", 26 | "backpack-core/babel" 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /layouts/container.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /assets/style/_transitions.scss: -------------------------------------------------------------------------------- 1 | .fade-enter-active, 2 | .fade-leave-active { 3 | transition: opacity 350ms ease-in; 4 | } 5 | 6 | .fade-enter, 7 | .fade-leave-to { 8 | opacity: 0; 9 | } 10 | 11 | .fade-up-enter-active, 12 | .fade-up-leave-active { 13 | transition: all .3s ease; 14 | } 15 | 16 | .fade-up-enter, 17 | .fade-up-leave-to { 18 | -webkit-transform: translateY(20px); 19 | transform: translateY(20px); 20 | opacity: 0; 21 | } 22 | 23 | .slide-in-enter-active, 24 | .slide-in-leave-active { 25 | transition: opacity 350ms ease-in; 26 | } 27 | 28 | .slide-in-enter, 29 | .slide-in-leave-to { 30 | opacity: 0; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /middleware/is-admin.js: -------------------------------------------------------------------------------- 1 | import localConfig from '@/local.config' 2 | import isEmpty from 'lodash/isEmpty' 3 | 4 | /** 5 | * Middleware to check if the current user is admin. 6 | * @param {Object} context 7 | * The nuxt context. 8 | */ 9 | export default function ({ store, redirect, error }) { 10 | const currentUser = store.state.currentUser 11 | if (!currentUser || isEmpty(currentUser)) { 12 | redirect(`/account/signin`) 13 | } else if (!currentUser.admin) { 14 | error({ 15 | message: `Sorry, this page can only be accessed by ${localConfig.brand} 16 | administrators.`, 17 | statusCode: 403 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /middleware/is-current-or-admin.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty' 2 | 3 | /** 4 | * Middleware to check if the current user is admin or the owner of a page. 5 | * @param {Object} context 6 | * The nuxt context. 7 | */ 8 | export default function ({ store, route, redirect, error }) { 9 | const name = route.params.name 10 | const currentUser = store.state.currentUser 11 | if (isEmpty(currentUser)) { 12 | redirect(`/account/signin?next=${route.path}`) 13 | } else if (!currentUser.admin && !currentUser.name === name) { 14 | error({ 15 | message: 'Sorry, access to this page is restricted.', 16 | statusCode: 403 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /utils/getDefaultEmail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Validate an email address and return a default if invalid. 3 | * 4 | * Primarily used for cases where users signed up to PYBOSSA with SSO, yet 5 | * we need a valid email address to create their account in Flarum. So, we 6 | * just generate a fake one at example.com for now. We can revisit if Flarum 7 | * later turns of the requirement of a valid email address. 8 | * @param {String} email 9 | * The email address. 10 | */ 11 | export const getDefaultEmail = function (email) { 12 | const re = new RegExp(/[^@]+@[^@]+\.[^@]+/) 13 | if (re.test(email)) { 14 | return email 15 | } 16 | return email + '@example.com' 17 | } 18 | -------------------------------------------------------------------------------- /plugins/cookie-consent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show cookie consent on the client only. 3 | */ 4 | export default ({ app }) => { 5 | if (process.client) { 6 | require('cookieconsent') 7 | const cookiesPolicy = app.router.resolve({ name: 'help-cookies' }).href 8 | window.cookieconsent.initialise({ 9 | content: { 10 | message: 'This website uses cookies to ensure you get the best ' + 'experience (mmm...cookies).', 11 | href: window.location.origin + cookiesPolicy}, 12 | palette: { 13 | popup: { 14 | background: 'rgba(0,0,0,0.8)'}, 15 | button: { 16 | background: '#0552b1' 17 | } 18 | } 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/unit/specs/pages/help/api.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | 3 | import { mount, createLocalVue } from '@vue/test-utils' 4 | import ApiHelp from '@/pages/help/api' 5 | 6 | describe('API help page', () => { 7 | let localVue = null 8 | let wrapper = null 9 | 10 | beforeEach(() => { 11 | localVue = createLocalVue() 12 | localVue.use(BootstrapVue) 13 | wrapper = mount(ApiHelp, { 14 | localVue 15 | }) 16 | }) 17 | 18 | it('renders correctly', () => { 19 | const renderer = require('vue-server-renderer').createRenderer() 20 | renderer.renderToString(wrapper.vm, (err, str) => { 21 | expect(str).toMatchSnapshot() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/unit/specs/pages/help/tos.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | 3 | import { mount, createLocalVue } from '@vue/test-utils' 4 | import TosHelp from '@/pages/help/tos' 5 | 6 | describe('API help page', () => { 7 | let localVue = null 8 | let wrapper = null 9 | 10 | beforeEach(() => { 11 | localVue = createLocalVue() 12 | localVue.use(BootstrapVue) 13 | wrapper = mount(TosHelp, { 14 | localVue 15 | }) 16 | }) 17 | 18 | it('renders correctly', () => { 19 | const renderer = require('vue-server-renderer').createRenderer() 20 | renderer.renderToString(wrapper.vm, (err, str) => { 21 | expect(str).toMatchSnapshot() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/unit/specs/pages/help/cookies.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | 3 | import { mount, createLocalVue } from '@vue/test-utils' 4 | import CookiesHelp from '@/pages/help/cookies' 5 | 6 | describe('API help page', () => { 7 | let localVue = null 8 | let wrapper = null 9 | 10 | beforeEach(() => { 11 | localVue = createLocalVue() 12 | localVue.use(BootstrapVue) 13 | wrapper = mount(CookiesHelp, { 14 | localVue 15 | }) 16 | }) 17 | 18 | it('renders correctly', () => { 19 | const renderer = require('vue-server-renderer').createRenderer() 20 | renderer.renderToString(wrapper.vm, (err, str) => { 21 | expect(str).toMatchSnapshot() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/unit/specs/pages/help/privacy.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | 3 | import { mount, createLocalVue } from '@vue/test-utils' 4 | import PrivacyHelp from '@/pages/help/privacy' 5 | 6 | describe('API help page', () => { 7 | let localVue = null 8 | let wrapper = null 9 | 10 | beforeEach(() => { 11 | localVue = createLocalVue() 12 | localVue.use(BootstrapVue) 13 | wrapper = mount(PrivacyHelp, { 14 | localVue 15 | }) 16 | }) 17 | 18 | it('renders correctly', () => { 19 | const renderer = require('vue-server-renderer').createRenderer() 20 | renderer.renderToString(wrapper.vm, (err, str) => { 21 | expect(str).toMatchSnapshot() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /middleware/project-management.js: -------------------------------------------------------------------------------- 1 | import localConfig from '@/local.config' 2 | import isEmpty from 'lodash/isEmpty' 3 | 4 | /** 5 | * Middleware to check if the current user is authorised to manage projects. 6 | * @param {Object} context 7 | * The nuxt context. 8 | */ 9 | export default function ({ store, redirect, error }) { 10 | const currentUser = store.state.currentUser 11 | if (!currentUser || isEmpty(currentUser)) { 12 | redirect(`/account/signin`) 13 | } else if (localConfig.disableProjectBuilder && !currentUser.admin) { 14 | error({ 15 | message: `Sorry, this page can only be accessed by ${localConfig.brand} 16 | administrators.`, 17 | statusCode: 403 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mixins/exportFile.js: -------------------------------------------------------------------------------- 1 | import FileSaver from 'file-saver' 2 | 3 | export const exportFile = { 4 | methods: { 5 | /** 6 | * Export data as a zip file. 7 | * @param {data} data 8 | * The data. 9 | * @param {String} prefix 10 | * The filename prefix. 11 | * @param {String} format 12 | * The data format. 13 | */ 14 | exportFile (data, prefix, format) { 15 | const types = { 16 | 'zip': 'application/zip', 17 | 'csv': 'text/csv', 18 | 'json': 'application/json' 19 | } 20 | const blob = new Blob([data], { type: types[format] }) 21 | const fn = `${prefix}.${format}` 22 | return FileSaver.saveAs(blob, fn) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/nuxt-explicates/module.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // Javascript client for the Explicates Web Annotation server 4 | // https://github.com/alexandermendes/explicates 5 | 6 | module.exports = function nuxtExplicates (_modOptions) { 7 | // Combine options 8 | const modOptions = Object.assign({}, this.options.explicates, _modOptions) 9 | 10 | // Apply defaults (currently no defaults) 11 | const options = Object.assign({ 12 | accept: 'application/ld+json; profile="http://www.w3.org/ns/anno.jsonld"' 13 | }, modOptions) 14 | 15 | // Register plugin 16 | this.addPlugin({ 17 | src: path.resolve(__dirname, 'plugin.template.js'), 18 | fileName: 'explicates.js', 19 | options: options 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /pages/account/signout.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /pages/account/register/confirmation.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 34 | -------------------------------------------------------------------------------- /mixins/fetchProjectAndCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch a collection by the current project. 3 | */ 4 | export const fetchProjectAndCollection = { 5 | fetch ({ params, app, error, store }) { 6 | return app.$axios.$get('/api/project', { 7 | params: { 8 | short_name: params.short_name, 9 | all: 1 10 | } 11 | }).then(data => { 12 | if (!data || data.length !== 1) { 13 | error({ statusCode: 404 }) 14 | } else { 15 | store.dispatch('UPDATE_CURRENT_PROJECT', data[0]) 16 | return app.$axios.$get(`/api/category/${data[0].category_id}`) 17 | } 18 | }).then(data => { 19 | store.dispatch('UPDATE_CURRENT_COLLECTION', data) 20 | }).catch(err => { 21 | error(err) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utils/batch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return an array of arrays, each a batch of n items. 3 | * @param {Array} items 4 | * The items. 5 | * @param {Number} n 6 | * The size of each batch. 7 | * @param {*} fillWith 8 | * The item to fill any incomplete batch with. 9 | */ 10 | export const batch = function (items, n, fillWith) { 11 | let batched = [] 12 | let tmp = [] 13 | for (let item of items) { 14 | if (tmp.length === n) { 15 | batched.push(tmp) 16 | tmp = [] 17 | } 18 | tmp.push(item) 19 | } 20 | if (tmp.length) { 21 | if (fillWith !== undefined && tmp.length < n) { 22 | for (let i = 0; i < n - tmp.length; i++) { 23 | tmp.push(fillWith) 24 | } 25 | } 26 | batched.push(tmp) 27 | } 28 | return batched 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | 'jest/globals': true 11 | }, 12 | extends: 'standard', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html', 16 | 'jest' 17 | ], 18 | // add your custom rules here 19 | rules: { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 26 | }, 27 | globals: {}, 28 | settings: { 29 | 'import/resolver': { 30 | 'babel-module': {} 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /layouts/collection-fullscreen-dark.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 37 | -------------------------------------------------------------------------------- /pages/admin/site/jobs.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /layouts/collection-default.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | -------------------------------------------------------------------------------- /plugins/axios.js: -------------------------------------------------------------------------------- 1 | export default function ({ $axios, error, req }) { 2 | $axios.onRequest(config => { 3 | config.headers['Content-Type'] = 'application/json' 4 | 5 | // Use any form data to set CSRF token header 6 | if (config.data && config.data.hasOwnProperty('csrf')) { 7 | config.headers['X-CSRFToken'] = config.data.csrf 8 | } 9 | 10 | // Ensure some data otherwise axois can delete Content-Type 11 | if (config.data === undefined) { 12 | config.data = {} 13 | } 14 | return config 15 | }) 16 | 17 | $axios.onError(err => { 18 | const errorParams = { 19 | statusCode: parseInt(err.response && err.response.status) || 500, 20 | message: ( 21 | err.message || 22 | err.response.statusText || 23 | 'Internal Server Error' 24 | ) 25 | } 26 | return Promise.reject(errorParams) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /utils/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return true if the PYBOSSA session is not for the user, false otherwise. 3 | * @param {Object} user 4 | * The user. 5 | * @param {String} cookieStr 6 | * The document cookie string. 7 | */ 8 | export const updateSession = (user, cookieStr) => { 9 | let allCookies = [] 10 | if (cookieStr) { 11 | allCookies = cookieStr.split(';') 12 | } 13 | 14 | let hasSession = false 15 | let sessionName = null 16 | 17 | // Get the username of the current session, if any 18 | for (let cookie of allCookies) { 19 | if (cookie.indexOf('remember_token=') > -1) { 20 | hasSession = true 21 | sessionName = cookie.split('|')[0].split('=')[1] 22 | } 23 | } 24 | 25 | // Return true if the user doesn't match the current session 26 | return ( 27 | ((user === null || user === undefined) === hasSession) || 28 | (hasSession && user.name !== sessionName) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /assets/style/_cards.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | .card-header { 3 | font-family: $headings-font-family; 4 | background-color: $white; 5 | } 6 | 7 | .tab-content.card-body { 8 | padding: 0; 9 | } 10 | 11 | &.options-card { 12 | font-size: $font-size-sm; 13 | margin-top: 0; 14 | 15 | .card-header { 16 | background-color: $gray-100; 17 | text-align: center; 18 | padding: $list-group-item-padding-y $list-group-item-padding-x; 19 | } 20 | } 21 | 22 | .search-control { 23 | border-radius: 100px; 24 | padding: 0.5rem 0.75rem; 25 | } 26 | 27 | &.card-overflow { 28 | overflow: visible; 29 | } 30 | 31 | &.bg-dark { 32 | background-color: $gray-1000 !important; 33 | border-color: $gray-900; 34 | 35 | .card-header { 36 | background-color: $gray-1000 !important; 37 | } 38 | 39 | &.text-white { 40 | color: $gray-200; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import express from 'express' 3 | import { Nuxt, Builder } from 'nuxt' 4 | 5 | const host = process.env.HOST || '127.0.0.1' 6 | const port = process.env.PORT || 8080 7 | 8 | // Init express 9 | const app = express() 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 | // Init Nuxt.js 17 | const nuxt = new Nuxt(config) 18 | app.use(nuxt.render) 19 | 20 | // Build only in dev mode 21 | if (config.dev) { 22 | const builder = new Builder(nuxt) 23 | builder.build().then(listen) 24 | } else { 25 | listen() 26 | } 27 | 28 | // Listen to the server 29 | function listen () { 30 | app.listen(port, host).on('error', function (err) { 31 | console.log(chalk.red(err)) 32 | }) 33 | console.log(chalk.green('Server listening on ' + host + ':' + port)) 34 | } 35 | -------------------------------------------------------------------------------- /layouts/collection-tabs.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | -------------------------------------------------------------------------------- /assets/style/_tables.scss: -------------------------------------------------------------------------------- 1 | .table { 2 | display: table; 3 | width: 100%; 4 | font-size: $font-size-sm; 5 | margin-bottom: 0; 6 | background-color: $white; 7 | border-bottom: $table-border-width solid $table-border-color; 8 | 9 | &.table-dark { 10 | color: $gray-200; 11 | background-color: $dark; 12 | border: $table-border-width solid $dark; 13 | } 14 | 15 | th { 16 | font-weight: 400; 17 | text-transform: uppercase; 18 | color: $text-muted; 19 | } 20 | 21 | td { 22 | vertical-align: middle; 23 | } 24 | 25 | .btn { 26 | margin: 0.25rem; 27 | 28 | @include media-breakpoint-down(md) { 29 | display: block; 30 | width: 100%; 31 | } 32 | } 33 | } 34 | 35 | // To stop tables loading before the user scrolls to the bottom 36 | // As the bottom is flexible and no max-height is set 37 | .dashboard { 38 | .infinite-loading-table { 39 | max-height: 65vh; 40 | overflow: auto; 41 | } 42 | } -------------------------------------------------------------------------------- /assets/style/_charts.scss: -------------------------------------------------------------------------------- 1 | .ct-container { 2 | height: 100%; 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | overflow: hidden; 7 | overflow: visible; 8 | 9 | @include media-breakpoint-up(lg) { 10 | flex-direction: row; 11 | } 12 | } 13 | 14 | .ct-legend { 15 | font-size: $font-size-sm; 16 | list-style: none; 17 | position: relative; 18 | margin: 0; 19 | 20 | li { 21 | position: relative; 22 | text-align: left; 23 | } 24 | 25 | li:before { 26 | width: 12px; 27 | height: 12px; 28 | margin: 3px; 29 | position: absolute; 30 | content: ''; 31 | border: 3px solid transparent; 32 | border-radius: 2px; 33 | left: -23px; 34 | top: 2px; 35 | } 36 | 37 | @for $i from 0 to length($ct-series-colors) { 38 | .ct-series-#{$i}:before { 39 | background-color: nth($ct-series-colors, $i + 1); 40 | border-color: nth($ct-series-colors, $i + 1); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /test/unit/specs/components/footers/dashboard.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | 3 | import Icon from 'vue-awesome/components/Icon' 4 | import 'vue-awesome/icons/twitter' 5 | import 'vue-awesome/icons/github' 6 | import 'vue-awesome/icons/envelope' 7 | 8 | import { mount, createLocalVue } from '@vue/test-utils' 9 | import DashboardFooter from '@/components/footers/Dashboard' 10 | 11 | describe('Dashboard footer', () => { 12 | let localVue = null 13 | let wrapper = null 14 | 15 | beforeEach(() => { 16 | localVue = createLocalVue() 17 | localVue.use(BootstrapVue) 18 | localVue.component('icon', Icon) 19 | wrapper = mount(DashboardFooter, { 20 | localVue 21 | }) 22 | }) 23 | 24 | it('renders correctly', () => { 25 | const renderer = require('vue-server-renderer').createRenderer() 26 | renderer.renderToString(wrapper.vm, (err, str) => { 27 | expect(str).toMatchSnapshot() 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /mixins/hideCookieConsent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use sparingly to hide the cookie consent banner. 3 | */ 4 | 5 | /* 6 | GN: add or remove a class on the first DOM element matching a given selector. 7 | */ 8 | function addRemoveClass (selector, className, remove) { 9 | let element = document.querySelector(selector) 10 | if (!element) return 11 | let classList = element.classList 12 | if (!classList) return 13 | 14 | if (remove) { 15 | classList.remove(className) 16 | } else { 17 | classList.add(className) 18 | } 19 | } 20 | 21 | export const hideCookieConsent = { 22 | mounted () { 23 | if (process.browser) { 24 | // document.querySelector('.cc-window').style.opacity = 0 25 | addRemoveClass('.cc-window', 'cc-suspended') 26 | } 27 | }, 28 | 29 | beforeDestroy () { 30 | if (process.browser) { 31 | // document.querySelector('.cc-window').style.opacity = 1 32 | addRemoveClass('.cc-window', 'cc-suspended', true) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mixins/deleteDomainObject.js: -------------------------------------------------------------------------------- 1 | import capitalize from 'capitalize' 2 | 3 | export const deleteDomainObject = { 4 | methods: { 5 | deleteDomainObject (type, id, callback) { 6 | const object = type === 'category' ? 'collection' : type 7 | this.$swal({ 8 | title: `Delete ${capitalize(object)}`, 9 | text: `Are you sure you want to delete this ${object}?`, 10 | type: 'warning', 11 | showCancelButton: true, 12 | reverseButtons: true, 13 | showLoaderOnConfirm: true, 14 | preConfirm: () => { 15 | return this.$axios.$delete(`/api/${type}/${id}`) 16 | } 17 | }).then(result => { 18 | if (result) { 19 | this.$notifications.success({ 20 | message: `${capitalize(object)} deleted` 21 | }) 22 | callback() 23 | } 24 | }).catch(err => { 25 | if (typeof err === 'object' && err.hasOwnProperty('dismiss')) { 26 | this.$nuxt.error(err) 27 | } 28 | }) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/unit/specs/components/charts/bar.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | import VueChartist from 'vue-chartist' 3 | import 'chartist-plugin-tooltips' 4 | import noSSR from '@/.nuxt/components/no-ssr' 5 | 6 | import { mount, createLocalVue } from '@vue/test-utils' 7 | import BarChart from '@/components/charts/Bar' 8 | 9 | describe('Bar chart', () => { 10 | let localVue = null 11 | let wrapper = null 12 | 13 | beforeEach(() => { 14 | localVue = createLocalVue() 15 | localVue.use(BootstrapVue) 16 | localVue.use(VueChartist) 17 | localVue.component(noSSR.name, noSSR) 18 | wrapper = mount(BarChart, { 19 | localVue, 20 | propsData: { 21 | chartData: { 22 | labels: ['A', 'B', 'C'], 23 | series: [[1, 3, 2], [4, 6, 5]] 24 | }, 25 | unit: 'task' 26 | } 27 | }) 28 | }) 29 | 30 | it('renders correctly', () => { 31 | const renderer = require('vue-server-renderer').createRenderer() 32 | renderer.renderToString(wrapper.vm, (err, str) => { 33 | expect(str).toMatchSnapshot() 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/unit/specs/components/charts/line.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | import VueChartist from 'vue-chartist' 3 | import 'chartist-plugin-tooltips' 4 | import noSSR from '@/.nuxt/components/no-ssr' 5 | 6 | import { mount, createLocalVue } from '@vue/test-utils' 7 | import LineChart from '@/components/charts/Line' 8 | 9 | describe('Line chart', () => { 10 | let localVue = null 11 | let wrapper = null 12 | 13 | beforeEach(() => { 14 | localVue = createLocalVue() 15 | localVue.use(BootstrapVue) 16 | localVue.use(VueChartist) 17 | localVue.component(noSSR.name, noSSR) 18 | wrapper = mount(LineChart, { 19 | localVue, 20 | propsData: { 21 | chartData: { 22 | labels: ['A', 'B', 'C'], 23 | series: [[1, 3, 2], [4, 6, 5]] 24 | }, 25 | unit: 'task' 26 | } 27 | }) 28 | }) 29 | 30 | it('renders correctly', () => { 31 | const renderer = require('vue-server-renderer').createRenderer() 32 | renderer.renderToString(wrapper.vm, (err, str) => { 33 | expect(str).toMatchSnapshot() 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibCrowds 2 | 3 | [![Build Status](https://travis-ci.org/LibCrowds/libcrowds.svg?branch=master)](https://travis-ci.org/LibCrowds/libcrowds) 4 | [![Coverage Status](https://coveralls.io/repos/github/LibCrowds/libcrowds/badge.svg?branch=master)](https://coveralls.io/github/LibCrowds/libcrowds?branch=master) 5 | [![DOI](https://zenodo.org/badge/92406558.svg)](https://zenodo.org/badge/latestdoi/92406558) 6 | 7 | > A Vue.js frontend for PyBossa (>=2.8.0). 8 | 9 | The LibCrowds package is a Vue.js UI that communicates with a PYBOSSA backend to present crowdsourcing projects using a microsite-based structure. 10 | 11 | [**Read the documentation**](https://docs.libcrowds.com) 12 | 13 | [![The LibCrowds website](./assets/img/site-homepage.png?raw=true "The LibCrowds website")](https://www.libcrowds.com) 14 | 15 | ## Build Setup 16 | 17 | ``` bash 18 | # install dependencies 19 | $ npm install 20 | 21 | # serve with hot reload at localhost:8080 22 | $ npm run dev 23 | 24 | # build for production and launch server 25 | $ npm run build 26 | $ npm start 27 | ``` 28 | 29 | [**Read the documentation**](https://docs.libcrowds.com) -------------------------------------------------------------------------------- /mixins/fetchCollectionAndTmpl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch collection by short_name param and template by ID, then update store. 3 | */ 4 | export const fetchCollectionAndTmpl = { 5 | fetch ({ params, app, error, store }) { 6 | return app.$axios.$get('/api/category', { 7 | params: { 8 | short_name: params.short_name 9 | } 10 | }).then(data => { 11 | // Collection not found 12 | if (!data || data.length !== 1) { 13 | error({ statusCode: 404 }) 14 | return 15 | } 16 | 17 | store.dispatch('UPDATE_CURRENT_COLLECTION', data[0]) 18 | 19 | // Get template (store used as we then get the defaults set) 20 | let template = null 21 | for (let tmpl of store.state.currentCollection.info.templates) { 22 | if (tmpl['id'] === params.id) { 23 | template = tmpl 24 | } 25 | } 26 | 27 | // Template not found 28 | if (!template) { 29 | error({ statusCode: 404 }) 30 | return 31 | } 32 | 33 | store.dispatch('UPDATE_CURRENT_TEMPLATE', template) 34 | }).catch(err => { 35 | error(err) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /components/buttons/ProjectContrib.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 53 | -------------------------------------------------------------------------------- /plugins/cookies.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Cookie from 'cookie' 3 | import JSCookie from 'js-cookie' 4 | 5 | // Called only on client-side 6 | export const getCookies = (str) => { 7 | return Cookie.parse(str || '') 8 | } 9 | 10 | /* 11 | ** Executed by ~/.nuxt/index.js with context given 12 | ** This method can be asynchronous 13 | */ 14 | export default ({ req }, inject) => { 15 | // Inject `cookies` key 16 | // -> app.$cookies 17 | // -> this.$cookies in vue components 18 | // -> this.$cookies in store actions/mutations 19 | inject('cookies', new Vue({ 20 | data () { 21 | return { 22 | cookies: getCookies( 23 | process.server 24 | ? req.headers.cookie 25 | : document.cookie 26 | ) 27 | } 28 | }, 29 | methods: { 30 | set (...args) { 31 | JSCookie.set(...args) 32 | this.cookies = getCookies(document.cookie) 33 | }, 34 | get (key) { 35 | return this.cookies[key] 36 | }, 37 | remove (...args) { 38 | JSCookie.remove(...args) 39 | this.cookies = getCookies(document.cookie) 40 | } 41 | } 42 | })) 43 | } 44 | -------------------------------------------------------------------------------- /test/unit/specs/pages/collection/about.spec.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | import BootstrapVue from 'bootstrap-vue' 3 | import { mount, createLocalVue } from '@vue/test-utils' 4 | 5 | import About from '@/pages/collection/_short_name/about' 6 | 7 | describe('Collection about page', () => { 8 | let localVue = null 9 | let store = null 10 | let wrapper = null 11 | 12 | beforeEach(() => { 13 | localVue = createLocalVue() 14 | localVue.use(BootstrapVue) 15 | localVue.use(Vuex) 16 | store = new Vuex.Store({ 17 | state: { 18 | currentCollection: { 19 | info: { 20 | content: { 21 | about: '# About\n\nFoo' 22 | } 23 | } 24 | } 25 | }, 26 | actions: { 27 | UPDATE_COLLECTION_NAV_ITEMS: () => ({}) 28 | } 29 | }) 30 | }) 31 | 32 | it('renders correctly', () => { 33 | wrapper = mount(About, { 34 | localVue, 35 | store 36 | }) 37 | const renderer = require('vue-server-renderer').createRenderer() 38 | renderer.renderToString(wrapper.vm, (err, str) => { 39 | expect(str).toMatchSnapshot() 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /utils/fetchAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch all instances of a domain obejct from the PYBOSSA API. 3 | * @param {Object} axios 4 | * The application's axios instance. 5 | * @param {String} domainObject 6 | * The domain object to fetch. 7 | * @param {Object} params 8 | * Any HTTP parameters. 9 | */ 10 | export const fetchAll = async function (axios, domainObject, params = {}) { 11 | const endpoint = `/api/${domainObject}` 12 | let lastFetched = null 13 | let objects = [] 14 | 15 | while (objectsNotExhausted(lastFetched)) { 16 | const queryParams = JSON.parse(JSON.stringify(params)) 17 | queryParams.limit = 100 18 | 19 | if (lastFetched && lastFetched.length) { 20 | queryParams.last_id = lastFetched[lastFetched.length - 1].id 21 | } 22 | 23 | const data = await axios.$get(endpoint, { 24 | params: queryParams 25 | }) 26 | 27 | lastFetched = data 28 | objects = objects.concat(data) 29 | } 30 | return objects 31 | } 32 | 33 | function objectsNotExhausted (lastFetched) { 34 | return ( 35 | !lastFetched || 36 | ( 37 | lastFetched.length !== 0 && 38 | lastFetched.length === 100 39 | ) 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /assets/style/_forms.scss: -------------------------------------------------------------------------------- 1 | .form-base, 2 | .form-container, 3 | .form-base.form-container { 4 | border: none; 5 | 6 | label, 7 | .col-form-legend { 8 | font-weight: 600; 9 | font-size: $font-size-sm; 10 | } 11 | 12 | .errors { 13 | color: $red; 14 | font-size: $font-size-sm; 15 | 16 | span { 17 | margin-top: 0.25rem; 18 | display: block; 19 | } 20 | } 21 | 22 | fieldset { 23 | padding: 0; 24 | } 25 | 26 | .form-group { 27 | margin-bottom: 2rem; 28 | 29 | .hint, 30 | .form-text { 31 | margin-top: 0.25rem; 32 | font-size: $font-size-sm; 33 | color: $text-muted; 34 | } 35 | } 36 | } 37 | 38 | .input-group-append { 39 | input { 40 | border-radius: 0; 41 | } 42 | } 43 | 44 | .input-group-btn { 45 | input { 46 | border-radius: 0; 47 | } 48 | } 49 | 50 | .form-dark { 51 | .form-control, 52 | .custom-file-control { 53 | color: $gray-100; 54 | background-color: $gray-800; 55 | border-color: $gray-800; 56 | } 57 | 58 | .btn, 59 | .custom-file-control:before { 60 | color: $gray-100; 61 | background-color: $gray-1200; 62 | border-color: $gray-1200; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /components/buttons/Clipboard.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /components/charts/Pie.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 50 | -------------------------------------------------------------------------------- /test/fixtures/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 79, 3 | "info": { 4 | "thumbnail_url": "/uploads/user_362/project_79_thumbnail_1510222150.png", 5 | "container": "user_362", 6 | "filters": { 7 | "Task": "Transcribe", 8 | "Location": "Dublin", 9 | "Entity": "Dates" 10 | }, 11 | "volume_id": "7f101c63-ae39-49cb-bffe-cf2c6aff17e9", 12 | "template_id": "fe496028-d1a6-4b7d-b1fc-61df16a1f18f", 13 | "thumbnail": "project_79_thumbnail_1510222150.png" 14 | }, 15 | "updated": "2018-02-14T00:59:04.858600", 16 | "description": "Transcribe the dates of plays in historic playbills", 17 | "short_name": "transcribe_dates_theatre_royal_dublin_1825-1829_vol_1", 18 | "created": "2017-11-08T12:25:27.351084", 19 | "name": "Transcribe Dates: Theatre Royal, Dublin 1825-1829 (Vol. 1)", 20 | "links": [ 21 | "" 22 | ], 23 | "owner_id": 1, 24 | "featured": false, 25 | "link": "", 26 | "category_id": 22, 27 | "long_description": "", 28 | "owners_ids": [ 29 | 1, 30 | 1 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /components/charts/Bar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | -------------------------------------------------------------------------------- /layouts/help-dashboard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 56 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | import { getDefaultEmail } from '@/utils/getDefaultEmail' 3 | 4 | import mutations from '@/store/mutations' 5 | import actions from '@/store/actions' 6 | 7 | actions.nuxtServerInit = async ({ dispatch }, { app }) => { 8 | await dispatch('UPDATE_CURRENT_USER', app.$axios) 9 | await dispatch('UPDATE_PUBLISHED_COLLECTIONS', app.$axios) 10 | 11 | if (app.$cookies.get('dark-mode') === 'true') { 12 | await dispatch('TOGGLE_DARK_MODE') 13 | } 14 | } 15 | 16 | // nuxtClientInit is dispatched from plugins/nuxt-client-init 17 | actions.nuxtClientInit = ({ state }, { app }) => { 18 | const email = getDefaultEmail(state.currentUser.email_addr) 19 | const name = state.currentUser.name 20 | if (state.currentUser.id && app.$flarum) { 21 | app.$flarum.signin(name, email) 22 | } 23 | } 24 | 25 | export default () => { 26 | return new Vuex.Store({ 27 | state: { 28 | darkMode: false, 29 | currentUser: {}, 30 | publishedCollections: [], 31 | currentCollection: {}, 32 | collectionNavItems: [], 33 | currentProject: {}, 34 | currentTemplate: {} 35 | }, 36 | 37 | mutations, 38 | 39 | actions 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /mixins/licenses.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A mixin for the available licenses for data reuse, and their URLs. 3 | */ 4 | export const licenses = { 5 | data () { 6 | const baseUrl = 'https://creativecommons.org/licenses' 7 | const version = '4.0' 8 | return { 9 | dataLicenses: { 10 | CC0: { 11 | id: 'CC0', 12 | name: 'CC0', 13 | url: 'https://creativecommons.org/publicdomain/zero/1.0/' 14 | }, 15 | CC_BY: { 16 | name: 'CC0', 17 | url: `${baseUrl}/by/${version}/` 18 | }, 19 | CC_BY_SA: { 20 | name: 'CC BY-SA', 21 | url: `${baseUrl}/by-sa/${version}/` 22 | }, 23 | CC_BY_ND: { 24 | name: 'CC BY-ND', 25 | url: `${baseUrl}/by-nd/${version}/` 26 | }, 27 | CC_BY_NC: { 28 | name: 'CC BY-NC', 29 | url: `${baseUrl}/by-nc/${version}/` 30 | }, 31 | 32 | CC_BY_NC_SA: { 33 | name: 'CC BY-NC-SA', 34 | url: `${baseUrl}/by-nc-sa/${version}/` 35 | }, 36 | CC_BY_NC_ND: { 37 | name: 'CC_BY-NC-ND', 38 | url: `${baseUrl}/by-nc-nd/${version}/` 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/unit/specs/components/cards/profile.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | import VueGravatar from 'vue-gravatar' 3 | import Icon from 'vue-awesome/components/Icon' 4 | import 'vue-awesome/icons/trophy' 5 | import 'vue-awesome/icons/tasks' 6 | import 'vue-awesome/icons/clock-o' 7 | 8 | import pbTestResponses from '@/test/fixtures/pybossaTestResponses.json' 9 | import { mount, createLocalVue } from '@vue/test-utils' 10 | import ProfileCard from '@/components/cards/Profile' 11 | 12 | describe('Profile card', () => { 13 | let localVue = null 14 | let user = null 15 | let wrapper = null 16 | 17 | beforeEach(() => { 18 | localVue = createLocalVue() 19 | localVue.use(BootstrapVue) 20 | localVue.component('icon', Icon) 21 | localVue.component('v-gravatar', VueGravatar) 22 | user = pbTestResponses.getAccount.data.user 23 | wrapper = mount(ProfileCard, { 24 | localVue, 25 | propsData: { 26 | user: user 27 | } 28 | }) 29 | }) 30 | 31 | it('renders correctly', () => { 32 | const renderer = require('vue-server-renderer').createRenderer() 33 | renderer.renderToString(wrapper.vm, (err, str) => { 34 | expect(str).toMatchSnapshot() 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/unit/specs/pages/help/__snapshots__/api.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`API help page renders correctly 1`] = ` 4 |
5 |
6 |
7 |
8 |
API
9 |

10 | 11 | The My Brand API documentation. 12 | 13 |

14 |
15 | 16 |
17 |
18 | 19 |
20 |

21 | My Brand provides a RESTful API that can be used for retreiving information about tasks, task runs, results, etc. The API expects and returns JSON and is available at: 22 |

      
23 |         http://mylibcrowds.com
24 |       
25 |     
26 |

27 | Some requests will need an API key to authenticate and authorize the operation. You can find your API key listed in your profile. 28 |

29 |

30 | Rather than replicating the documentation here please refer to the official 31 | 32 | PYBOSSA API documentation 33 | 34 | for details. 35 |

36 |
37 |
38 | `; 39 | -------------------------------------------------------------------------------- /components/charts/Line.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 58 | -------------------------------------------------------------------------------- /test/unit/specs/components/cards/collection.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | import VueRouter from 'vue-router' 3 | import Icon from 'vue-awesome/components/Icon' 4 | import 'vue-awesome/icons/arrow-right' 5 | import 'vue-awesome/icons/circle-thin' 6 | import NuxtLink from '@/.nuxt/components/nuxt-link' 7 | 8 | import collection from '@/test/fixtures/collection.json' 9 | import { mount, createLocalVue } from '@vue/test-utils' 10 | import CollectionCard from '@/components/cards/Collection' 11 | import { routes } from '@/test/fixtures/routes' 12 | 13 | describe('Collection card', () => { 14 | let localVue = null 15 | let wrapper = null 16 | let router = null 17 | 18 | beforeEach(() => { 19 | localVue = createLocalVue() 20 | localVue.use(BootstrapVue) 21 | localVue.component('icon', Icon) 22 | localVue.component(NuxtLink.name, NuxtLink) 23 | localVue.use(VueRouter) 24 | router = new VueRouter({ 25 | routes 26 | }) 27 | wrapper = mount(CollectionCard, { 28 | localVue, 29 | router, 30 | propsData: { 31 | collection: collection 32 | } 33 | }) 34 | }) 35 | 36 | it('renders correctly', () => { 37 | const renderer = require('vue-server-renderer').createRenderer() 38 | renderer.renderToString(wrapper.vm, (err, str) => { 39 | expect(str).toMatchSnapshot() 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/unit/specs/components/footers/app.spec.js: -------------------------------------------------------------------------------- 1 | import VueRouter from 'vue-router' 2 | import BootstrapVue from 'bootstrap-vue' 3 | import NuxtLink from '@/.nuxt/components/nuxt-link' 4 | 5 | import Icon from 'vue-awesome/components/Icon' 6 | import 'vue-awesome/icons/twitter' 7 | import 'vue-awesome/icons/github' 8 | import 'vue-awesome/icons/envelope' 9 | 10 | import collection from '@/test/fixtures/collection.json' 11 | import { mount, createLocalVue } from '@vue/test-utils' 12 | import AppFooter from '@/components/footers/App' 13 | import { routes } from '@/test/fixtures/routes' 14 | 15 | describe('App footer', () => { 16 | let localVue = null 17 | let wrapper = null 18 | let router = null 19 | 20 | beforeEach(() => { 21 | localVue = createLocalVue() 22 | localVue.use(BootstrapVue) 23 | localVue.use(VueRouter) 24 | localVue.component('icon', Icon) 25 | localVue.component(NuxtLink.name, NuxtLink) 26 | router = new VueRouter({ 27 | routes 28 | }) 29 | wrapper = mount(AppFooter, { 30 | localVue, 31 | router, 32 | propsData: { 33 | collections: [ collection ] 34 | } 35 | }) 36 | }) 37 | 38 | it('renders correctly', () => { 39 | const renderer = require('vue-server-renderer').createRenderer() 40 | renderer.renderToString(wrapper.vm, (err, str) => { 41 | expect(str).toMatchSnapshot() 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /pages/collection/_short_name/projects/_id/index.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 48 | -------------------------------------------------------------------------------- /components/avatars/Base.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 70 | -------------------------------------------------------------------------------- /test/unit/specs/components/buttons/projectContrib.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | 3 | import collection from '@/test/fixtures/collection.json' 4 | import pbTestResponses from '@/test/fixtures/pybossaTestResponses.json' 5 | import { mount, createLocalVue } from '@vue/test-utils' 6 | import ProjectContribButton from '@/components/buttons/ProjectContrib' 7 | 8 | describe('ProjectContrib button', () => { 9 | let localVue = null 10 | let wrapper = null 11 | 12 | beforeEach(() => { 13 | localVue = createLocalVue() 14 | localVue.use(BootstrapVue) 15 | wrapper = mount(ProjectContribButton, { 16 | localVue, 17 | propsData: { 18 | collection: collection, 19 | project: pbTestResponses.getProject.data.project 20 | } 21 | }) 22 | }) 23 | 24 | it('is active by default', () => { 25 | const renderer = require('vue-server-renderer').createRenderer() 26 | renderer.renderToString(wrapper.vm, (err, str) => { 27 | expect(str).toMatchSnapshot() 28 | }) 29 | }) 30 | 31 | it('is disabled when project is completed', () => { 32 | let completedProject = { 33 | stats: { 34 | overall_progress: 100 35 | } 36 | } 37 | wrapper.setProps({ 38 | project: completedProject 39 | }) 40 | const renderer = require('vue-server-renderer').createRenderer() 41 | renderer.renderToString(wrapper.vm, (err, str) => { 42 | expect(str).toMatchSnapshot() 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /layouts/error.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 65 | -------------------------------------------------------------------------------- /pages/help/api.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 53 | -------------------------------------------------------------------------------- /components/data/DownloadAnnotationData.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /components/lists/ProjectFilters.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 66 | -------------------------------------------------------------------------------- /components/avatars/Project.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | 57 | 73 | -------------------------------------------------------------------------------- /local.config.js.tmpl: -------------------------------------------------------------------------------- 1 | const config = { 2 | brand: 'My Brand', 3 | company: 'My Company', 4 | description: 'My SEO optimised meta description', 5 | libcrowdsHost: process.env.NODE_ENV === 'development' 6 | ? 'http://127.0.0.1:8080' 7 | : 'http://xxxx.com', 8 | pybossaHost: process.env.NODE_ENV === 'development' 9 | ? 'http://127.0.0.1:5000' 10 | : 'http://xxxx.com', 11 | tagline: 'My inspiring tagline', 12 | explicates: { 13 | baseURL: 'http://127.0.0.1:3000' 14 | } 15 | } 16 | 17 | // Twitter 18 | // config.twitter = 'mytwitterhandle' 19 | 20 | // Contact email address 21 | // config.email = 'me@example.com' 22 | 23 | // GitHub account 24 | // config.github = 'https://github.com/LibCrowds/libcrowds' 25 | 26 | // Additional footer menu 27 | // config.footer = { 28 | // title: 'Newsletter', 29 | // items: [ 30 | // { text: 'Sign up', url: 'http://mailchimp.signup.url' } 31 | // ] 32 | // } 33 | 34 | // Sentry public DSN 35 | // config.sentry = { 36 | // public_key: '', 37 | // private_key: '', 38 | // project_id: '' 39 | // } 40 | 41 | // Google analytics tracking ID 42 | // config.analytics = { 43 | // ua: 'UA-XXX-X' 44 | // } 45 | 46 | // Documentation URI 47 | // config.docs = 'https://docs.example.com' 48 | 49 | // Facebook app ID 50 | // config.facebook: { 51 | // appId: '' 52 | // } 53 | 54 | // Flarum forum configuration 55 | // config.flarum = { 56 | // url: 'http://community.example.com', 57 | // sessionCookieDomain: '.example.com', 58 | // apiKey: '', 59 | // salt: 'super-secret-string' 60 | // } 61 | 62 | // Disable project building functions for non-admin users 63 | // config.disableProjectBuilder = true 64 | 65 | module.exports = config 66 | -------------------------------------------------------------------------------- /test/unit/specs/components/charts/pie.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | import VueChartist from 'vue-chartist' 3 | import 'chartist-plugin-tooltips' 4 | import noSSR from '@/.nuxt/components/no-ssr' 5 | 6 | import { mount, createLocalVue } from '@vue/test-utils' 7 | import PieChart from '@/components/charts/Pie' 8 | 9 | describe('Pie chart', () => { 10 | let localVue = null 11 | let wrapper = null 12 | let propsData = null 13 | let max = null 14 | let random = null 15 | 16 | beforeEach(() => { 17 | localVue = createLocalVue() 18 | localVue.use(BootstrapVue) 19 | localVue.use(VueChartist) 20 | localVue.component(noSSR.name, noSSR) 21 | max = 100 22 | random = Math.floor(Math.random() * (max - 1 + 1)) + 1 23 | propsData = { 24 | chartData: { 25 | labels: ['Auth', 'Anon'], 26 | series: [ 27 | { meta: 'Auth:', value: random }, 28 | { meta: 'Anon', value: max - random } 29 | ] 30 | }, 31 | unit: 'task' 32 | } 33 | wrapper = mount(PieChart, { 34 | localVue, 35 | propsData: propsData 36 | }) 37 | }) 38 | 39 | it('renders correctly', () => { 40 | const renderer = require('vue-server-renderer').createRenderer() 41 | renderer.renderToString(wrapper.vm, (err, str) => { 42 | expect(str).toMatchSnapshot() 43 | }) 44 | }) 45 | 46 | it('calculates the correct percentage for labels', () => { 47 | const idx = 0 48 | const label = propsData.chartData.labels[idx] 49 | const value = propsData.chartData.series[idx].value 50 | const expected = Math.round(value / max * 100) + '%' 51 | const result = wrapper.vm.labelInterpolationFnc(label, idx) 52 | expect(result).toBe(expected) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /pages/account/forgot-password.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 65 | -------------------------------------------------------------------------------- /test/unit/specs/components/cards/__snapshots__/collection.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Collection card renders correctly 1`] = ` 4 |
5 | 6 |
7 |
8 |

9 | In the Spotlight 10 |

11 |
12 |

13 | Bring past performances to life. 14 |

15 |
16 | 28 |
29 |
30 | `; 31 | -------------------------------------------------------------------------------- /assets/style/_images.scss: -------------------------------------------------------------------------------- 1 | .placeholder { 2 | height: 100%; 3 | width: 100%; 4 | background-color: $gray-100; 5 | color: $gray-600; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | &.placeholder-dark { 11 | background-color: $gray-800; 12 | color: $gray-300; 13 | } 14 | } 15 | 16 | .webp { 17 | .main-app-bg { 18 | background-image: url('~/assets/img/app-background.webp'); 19 | } 20 | 21 | .profile-card-bg { 22 | background: linear-gradient(to bottom, rgba($blue, 0) 0%, 23 | rgba($blue, 0) calc(50% + (#{$spacer} * 1)), 24 | $white calc(50% + (#{$spacer} * 1)), $white 100% ), 25 | url('~/assets/img/app-background.webp'); 26 | } 27 | 28 | .code-bg { 29 | background-image: url('~/assets/img/code.webp'); 30 | } 31 | 32 | .newton-bg { 33 | background-image: url('~/assets/img/newton.webp'); 34 | } 35 | } 36 | 37 | .no-webp { 38 | .main-app-bg { 39 | background-image: url('~/assets/img/app-background.jpg'); 40 | } 41 | 42 | .profile-card-bg { 43 | background: linear-gradient(to bottom, rgba($blue, 0) 0%, 44 | rgba($blue, 0) calc(50% + (#{$spacer} * 1)), 45 | $white calc(50% + (#{$spacer} * 1)), $white 100% ), 46 | url('~/assets/img/app-background.jpg'); 47 | } 48 | 49 | .code-bg { 50 | background-image: url('~/assets/img/code.png'); 51 | } 52 | 53 | .newton-bg { 54 | background-image: url('~/assets/img/newton.jpg'); 55 | } 56 | } 57 | 58 | .dark-mode { 59 | .profile-card-bg { 60 | background: linear-gradient(to bottom, rgba($blue, 0) 0%, 61 | rgba($blue, 0) calc(50% + (#{$spacer} * 1)), 62 | $dark calc(50% + (#{$spacer} * 1)), $dark 100% ), 63 | url('~/assets/img/app-background.jpg'); 64 | } 65 | 66 | .img-thumbnail { 67 | background: transparent; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pages/admin/collection/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 75 | -------------------------------------------------------------------------------- /pages/admin/template/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 70 | -------------------------------------------------------------------------------- /components/avatars/User.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 79 | -------------------------------------------------------------------------------- /pages/admin/project/_short_name/webhooks.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 74 | -------------------------------------------------------------------------------- /pages/collection/_short_name/about.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 73 | 74 | 84 | -------------------------------------------------------------------------------- /layouts/account-dashboard.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 82 | -------------------------------------------------------------------------------- /pages/account/_name/settings/preferences.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 79 | -------------------------------------------------------------------------------- /pages/admin/project/new/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 78 | -------------------------------------------------------------------------------- /components/lists/ItemTags.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 94 | -------------------------------------------------------------------------------- /test/unit/specs/components/buttons/oauth.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | import Icon from 'vue-awesome/components/Icon' 3 | import 'vue-awesome/icons' 4 | import 'vue-awesome/icons/facebook' 5 | import 'vue-awesome/icons/google-plus' 6 | import 'vue-awesome/icons/twitter' 7 | 8 | import testLocalConfig from '@/test/test.local.config' 9 | import { mount, createLocalVue } from '@vue/test-utils' 10 | import OauthButtons from '@/components/buttons/Oauth' 11 | 12 | describe('Oauth buttons', () => { 13 | let localVue = null 14 | let next = null 15 | let wrapper = null 16 | let propsData = null 17 | 18 | beforeEach(() => { 19 | localVue = createLocalVue() 20 | localVue.use(BootstrapVue) 21 | localVue.component('icon', Icon) 22 | next = '/example' 23 | propsData = { 24 | facebook: true, 25 | google: true, 26 | twitter: true, 27 | next: next 28 | } 29 | wrapper = mount(OauthButtons, { 30 | localVue, 31 | propsData: propsData 32 | }) 33 | }) 34 | 35 | it('does not render disabled buttons', () => { 36 | const renderer = require('vue-server-renderer').createRenderer() 37 | wrapper.setProps({ 38 | facebook: false, 39 | google: false, 40 | twitter: false 41 | }) 42 | renderer.renderToString(wrapper.vm, (err, str) => { 43 | expect(str).toMatchSnapshot() 44 | }) 45 | }) 46 | 47 | it('renders all enabled buttons', () => { 48 | const renderer = require('vue-server-renderer').createRenderer() 49 | renderer.renderToString(wrapper.vm, (err, str) => { 50 | expect(str).toMatchSnapshot() 51 | }) 52 | }) 53 | 54 | it('redirects to the correct oauth endpoint', () => { 55 | const endpoint = 'twitter' 56 | const host = testLocalConfig.pybossaHost 57 | const expectedUrl = `${host}/${endpoint}?response_format=json` 58 | const mockAssign = jest.fn() 59 | window.location.assign = mockAssign 60 | wrapper.vm.redirect(endpoint) 61 | expect(mockAssign).toHaveBeenCalledWith(expectedUrl) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /test/unit/specs/components/buttons/__snapshots__/oauth.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Oauth buttons does not render disabled buttons 1`] = ` 4 |
5 | 6 | 7 | 8 |
9 | `; 10 | 11 | exports[`Oauth buttons renders all enabled buttons 1`] = ` 12 |
13 | 20 | 27 | 34 |
35 | `; 36 | -------------------------------------------------------------------------------- /components/modals/Leaderboard.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 78 | 79 | 87 | -------------------------------------------------------------------------------- /pages/account/_name/settings/api.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 85 | -------------------------------------------------------------------------------- /components/cards/Base.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 84 | 85 | -------------------------------------------------------------------------------- /components/cards/Profile.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 58 | 59 | 92 | -------------------------------------------------------------------------------- /components/forms/Base.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 82 | 83 | 97 | -------------------------------------------------------------------------------- /pages/account/_name/announcements.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 76 | -------------------------------------------------------------------------------- /pages/account/newsletter.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 77 | -------------------------------------------------------------------------------- /test/unit/specs/utils/auth.spec.js: -------------------------------------------------------------------------------- 1 | import { updateSession } from '@/utils/auth' 2 | 3 | describe('updateSession', () => { 4 | let currentUser = null 5 | let name = null 6 | let otherCookie = null 7 | let sessionCookie = null 8 | let diffSessionCookie = null 9 | 10 | beforeEach(() => { 11 | name = 'joebloggs' 12 | currentUser = { 13 | name: name 14 | } 15 | sessionCookie = `remember_token=${name}|123` 16 | diffSessionCookie = `remember_token=someoneelse|123` 17 | otherCookie = 'session=abc123' 18 | }) 19 | 20 | it('returns true if there is a session and no current user', () => { 21 | const update = updateSession(null, sessionCookie) 22 | expect(update).toBe(true) 23 | }) 24 | 25 | it('returns true if no session and a current user', () => { 26 | const update = updateSession(currentUser, '') 27 | expect(update).toBe(true) 28 | }) 29 | 30 | it('returns true if session is for a different user', () => { 31 | const update = updateSession(currentUser, diffSessionCookie) 32 | expect(update).toBe(true) 33 | }) 34 | 35 | it('returns false if no session and no current user', () => { 36 | const update = updateSession(null, '') 37 | expect(update).toBe(false) 38 | }) 39 | 40 | it('returns false if session is for the current user', () => { 41 | const update = updateSession(currentUser, sessionCookie) 42 | expect(update).toBe(false) 43 | }) 44 | 45 | it('returns true if session and no current user and multiple cookies', () => { 46 | let cookies = [otherCookie, sessionCookie, otherCookie].join(';') 47 | const update = updateSession(null, cookies) 48 | expect(update).toBe(true) 49 | }) 50 | 51 | it('returns true if session is for another user and multiple cookies', () => { 52 | let cookies = [otherCookie, diffSessionCookie, otherCookie].join(';') 53 | const update = updateSession(currentUser, cookies) 54 | expect(update).toBe(true) 55 | }) 56 | 57 | it('returns false if session matches and multiple cookies', () => { 58 | let cookies = [otherCookie, sessionCookie, otherCookie].join(';') 59 | const update = updateSession(currentUser, cookies) 60 | expect(update).toBe(false) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /components/footers/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 54 | 55 | 76 | -------------------------------------------------------------------------------- /pages/account/_name/settings/avatar.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 82 | -------------------------------------------------------------------------------- /pages/admin/project/_short_name/delete.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 86 | -------------------------------------------------------------------------------- /pages/admin/project/_short_name/thumbnail.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 83 | -------------------------------------------------------------------------------- /pages/admin/template/_short_name/index.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 95 | -------------------------------------------------------------------------------- /pages/admin/template/_short_name/_id/tutorial.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 96 | -------------------------------------------------------------------------------- /components/data/FilterProjects.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 93 | -------------------------------------------------------------------------------- /mixins/currentMicrositeNavItems.js: -------------------------------------------------------------------------------- 1 | import localConfig from '@/local.config' 2 | 3 | /** 4 | * A mixin for the current collection microsite nav items. 5 | */ 6 | export const currentMicrositeNavItems = { 7 | computed: { 8 | currentCollection () { 9 | return this.$store.state.currentCollection 10 | }, 11 | 12 | forumUrl () { 13 | const baseUrl = localConfig.flarum.url 14 | if ( 15 | baseUrl && 16 | this.currentCollection.info.forum && 17 | this.currentCollection.info.forum.tag 18 | ) { 19 | return baseUrl + '/t/' + this.currentCollection.info.forum.tag 20 | } 21 | }, 22 | 23 | currentMicrositeNavItems () { 24 | const isMicrositePath = this.$route.path.indexOf('/collection') === 0 25 | 26 | if (!this.currentCollection.id || !isMicrositePath) { 27 | return [] 28 | } 29 | 30 | const items = [ 31 | { 32 | label: 'Home', 33 | link: { 34 | name: 'collection-short_name', 35 | params: { 36 | short_name: this.currentCollection.short_name 37 | } 38 | } 39 | }, 40 | { 41 | label: 'About', 42 | link: { 43 | name: 'collection-short_name-about', 44 | params: { 45 | short_name: this.currentCollection.short_name 46 | } 47 | } 48 | }, 49 | { 50 | label: 'Take Part', 51 | link: { 52 | name: 'collection-short_name-projects', 53 | params: { 54 | short_name: this.currentCollection.short_name 55 | } 56 | } 57 | }, 58 | { 59 | label: 'Data', 60 | link: { 61 | name: 'collection-short_name-data', 62 | params: { 63 | short_name: this.currentCollection.short_name 64 | } 65 | } 66 | }, 67 | { 68 | label: 'Browse Tags', 69 | link: { 70 | name: 'collection-short_name-browse', 71 | params: { 72 | short_name: this.currentCollection.short_name 73 | } 74 | } 75 | } 76 | ] 77 | 78 | // Add the forum URL if configured 79 | if (typeof this.forumUrl !== 'undefined') { 80 | items.push({ 81 | label: 'Discuss', 82 | external: true, 83 | link: this.forumUrl 84 | }) 85 | } 86 | 87 | return items 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/unit/specs/components/buttons/__snapshots__/socialMedia.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SocialMedia buttons renders correctly 1`] = ` 4 |
5 | 11 | 17 | 23 | 29 |
30 | `; 31 | -------------------------------------------------------------------------------- /pages/admin/collection/_short_name/delete.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 91 | -------------------------------------------------------------------------------- /pages/admin/project/_short_name/volume.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 93 | -------------------------------------------------------------------------------- /pages/account/_name/settings/security.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 88 | -------------------------------------------------------------------------------- /plugins/notifications.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueNotifications from 'vue-notifications' 3 | import swal from 'sweetalert2' 4 | import iziToast from 'izitoast' 5 | import capitalize from 'capitalize' 6 | 7 | function toast ({title, message, type, timeout, cb}) { 8 | if (type === VueNotifications.types.warn) type = 'warning' 9 | iziToast.settings({ 10 | position: 'bottomLeft' 11 | }) 12 | return iziToast[type]({ title, message, timeout }) 13 | } 14 | 15 | function sweetalert ({title, message, type, timeout, cb}) { 16 | if (type === VueNotifications.types.warn) type = 'warning' 17 | return swal(title, message, type) 18 | } 19 | 20 | const options = { 21 | success: toast, 22 | error: sweetalert, 23 | danger: toast, 24 | info: toast, 25 | warn: toast 26 | } 27 | 28 | Vue.use(VueNotifications, options) 29 | 30 | export default ({ req }, inject) => { 31 | // Inject `notifications` key 32 | // -> app.$notifications 33 | // -> this.$notifications in vue components 34 | // -> this.$notifications in store actions/mutations 35 | inject('notifications', new Vue({ 36 | notifications: { 37 | notify: {}, 38 | success: { 39 | title: 'Success', 40 | type: 'success' 41 | }, 42 | warn: { 43 | title: 'Warning', 44 | type: 'warn' 45 | }, 46 | info: { 47 | title: 'Info', 48 | type: 'info' 49 | }, 50 | error: { 51 | title: 'Error', 52 | type: 'error' 53 | }, 54 | invalidForm: { 55 | title: 'Invalid form data', 56 | type: 'warn', 57 | message: 'Please correct the errors' 58 | }, 59 | answerSaved: { 60 | title: 'Answer saved', 61 | type: 'success', 62 | message: 'Thank you for your contribution!' 63 | } 64 | }, 65 | 66 | methods: { 67 | /** 68 | * Handle flash messages. 69 | * @param {Object} data 70 | * The response data. 71 | */ 72 | flash (data) { 73 | if (process.browser && data !== undefined && 'flash' in data) { 74 | if (data.status === 'success') { 75 | this.success({ message: data.flash }) 76 | } else if (data.status === 'warning') { 77 | this.warn({ message: data.flash }) 78 | } else if (data.status === 'info' || data.status === 'message') { 79 | this.info({ message: data.flash }) 80 | } else { 81 | this.notify({ 82 | title: capitalize(data.status), 83 | type: data.status, 84 | message: data.flash 85 | }) 86 | } 87 | } 88 | } 89 | } 90 | })) 91 | } 92 | -------------------------------------------------------------------------------- /test/unit/specs/components/buttons/socialMedia.spec.js: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue' 2 | import Icon from 'vue-awesome/components/Icon' 3 | import 'vue-awesome/icons' 4 | import 'vue-awesome/icons/facebook' 5 | import 'vue-awesome/icons/google-plus' 6 | import 'vue-awesome/icons/twitter' 7 | import 'vue-awesome/icons/linkedin' 8 | 9 | import testLocalConfig from '@/test/test.local.config' 10 | import { mount, createLocalVue } from '@vue/test-utils' 11 | import SocialMediaButtons from '@/components/buttons/SocialMedia' 12 | 13 | describe('SocialMedia buttons', () => { 14 | let localVue = null 15 | let wrapper = null 16 | let shareUrl = null 17 | 18 | beforeEach(() => { 19 | localVue = createLocalVue() 20 | localVue.use(BootstrapVue) 21 | localVue.component('icon', Icon) 22 | shareUrl = 'http://example.com' 23 | wrapper = mount(SocialMediaButtons, { 24 | localVue, 25 | propsData: { 26 | shareUrl: shareUrl 27 | } 28 | }) 29 | }) 30 | 31 | it('renders correctly', () => { 32 | const renderer = require('vue-server-renderer').createRenderer() 33 | renderer.renderToString(wrapper.vm, (err, str) => { 34 | expect(str).toMatchSnapshot() 35 | }) 36 | }) 37 | 38 | it('sets the correct Facebook URL', () => { 39 | const url = `http://www.facebook.com/sharer.php?u=${shareUrl}` 40 | expect(wrapper.vm.facebookUrl).toEqual(url) 41 | }) 42 | 43 | it('sets the correct Twitter URL', () => { 44 | const url = `https://twitter.com/intent/tweet` + 45 | `?original_referer=${shareUrl}` + 46 | `&text=${testLocalConfig.tagline}&tw_p=tweetbutton&url=${shareUrl}` 47 | expect(wrapper.vm.twitterUrl).toEqual(url) 48 | }) 49 | 50 | it('sets the correct Google Plus URL', () => { 51 | const url = `https://plus.google.com/share?url=${shareUrl}` 52 | expect(wrapper.vm.googleplusUrl).toEqual(url) 53 | }) 54 | 55 | it('sets the correct LinkedIn URL', () => { 56 | const url = `https://www.linkedin.com/cws/share?url=${shareUrl}` 57 | expect(wrapper.vm.linkedinUrl).toEqual(url) 58 | }) 59 | 60 | it('opens a popup window with the correct share URL', () => { 61 | const url = `http://www.facebook.com/sharer.php?u=${shareUrl}` 62 | const width = 100 63 | const height = 200 64 | const specs = `scrollbars=yes,width=${width},height=${height}` 65 | const mockFocus = jest.fn() 66 | const mockOpen = jest.fn(() => { 67 | return { 68 | focus: mockFocus 69 | } 70 | }) 71 | window.open = mockOpen 72 | wrapper.vm.share('Facebook', url, width, height) 73 | expect(mockOpen).toHaveBeenCalledWith(url, 'Share', specs) 74 | expect(mockFocus).toHaveBeenCalled() 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/unit/specs/pages/help/__snapshots__/cookies.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`API help page renders correctly 1`] = ` 4 |
5 |
6 |
7 |
8 |
Cookies Policy
9 |

10 | 11 | The My Brand Cookies Policy. 12 | 13 |

14 |
15 | 16 |
17 |
18 | 19 |
20 |

21 | My Brand uses cookies to make our website easier for you to use and improve your overall experience, distinguish you from other users and provide increased functionality. 22 |

23 |

What are cookies?

24 |

25 | Cookies are small text files that websites save to your computer. They often include a randomly generated number which is stored on your device. Many cookies are automatically deleted after you finish using the website. Others remain on your computer 26 | to provide a seamless user experience – for example, by remembering which goods in an online shopping basket. 27 |

28 |

29 | Cookies are commonly used to improve the browsing experience; measure website performance; support the delivery of services; and support sharing information through social media platforms, such as Twitter or Facebook. 30 |

31 |

32 | Unless you have adjusted your browser settings to refuse cookies, our systems will issue cookies as soon you visit our website or access other online services. If you have switched off cookies then some of the functionality of our services may not be 33 | available to you. 34 |

35 |

36 | The following video from Google gives an explanation of how cookies work: 37 |

38 |
39 | 40 |
41 |

Disabling cookies

42 |

43 | You can block cookies by activating the setting on your browser that allows you to refuse all or some cookies. However, if you use your browser settings to block all cookies (including essential cookies) you may not be able to access parts of our website, 44 | or you may experience reduced functionality when accessing certain services. Unless you have adjusted your browser setting so that it will refuse cookies, our system will issue cookies as soon you visit our website. 45 |

46 |
47 |
48 | `; 49 | -------------------------------------------------------------------------------- /components/modals/AddProjectFilter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 100 | -------------------------------------------------------------------------------- /test/unit/specs/components/cards/__snapshots__/profile.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Profile card renders correctly 1`] = ` 4 |
5 |
6 |
7 |
8 | Avatar for JohnDoe 9 |
10 |
11 |
12 |
13 |
14 |

JohnDoe

15 |
16 |
17 |
    18 |
  • 19 | 20 | 21 | 22 | Rank 1813 23 |
  • 24 |
  • 25 | 26 | 27 | 28 | Contributions: 56 29 |
  • 30 |
  • 31 | 32 | 33 | 34 | Joined: 35 |
  • 36 |
37 |
38 |
39 |
40 | `; 41 | -------------------------------------------------------------------------------- /components/forms/Modal.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 110 | 111 | 116 | -------------------------------------------------------------------------------- /components/data/DownloadProjectData.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /pages/admin/site/announcements/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 106 | -------------------------------------------------------------------------------- /pages/account/_name/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 112 | -------------------------------------------------------------------------------- /pages/account/reset-password.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 104 | -------------------------------------------------------------------------------- /pages/admin/template/_short_name/_id/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 106 | -------------------------------------------------------------------------------- /pages/help/cookies.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 79 | -------------------------------------------------------------------------------- /pages/admin/collection/new.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 108 | -------------------------------------------------------------------------------- /pages/admin/template/_short_name/_id/parent.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 102 | -------------------------------------------------------------------------------- /components/forms/fields/Array.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 107 | -------------------------------------------------------------------------------- /layouts/bases/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 99 | 100 | 131 | --------------------------------------------------------------------------------