Browse Source

init

feature/yolo2
Paul Scheltema 9 months ago
parent
commit
3ec7ce2a4a
  1. 18
      .babelrc
  2. 24
      .eslintrc.js
  3. 2
      .nvmrc
  4. 4
      .prettierrc
  5. 26
      API
  6. 37
      BESPREKEN
  7. 55
      README.md
  8. 64
      TODO
  9. 17
      UREN
  10. BIN
      assets/logo.png
  11. 10
      ava.config.cjs
  12. 11
      components/NuxtLogo.vue
  13. 46
      components/Tutorial.vue
  14. 22
      components/VuetifyLogo.vue
  15. 48
      components/apps/list.vue
  16. 39
      components/contributor/list/item.vue
  17. 97
      components/contributors/editForm.vue
  18. 8
      components/logo/rug.vue
  19. 80
      components/navigation.vue
  20. 8
      components/researchStudies/createButton.vue
  21. 90
      components/researchStudies/list.vue
  22. 40
      components/ui/rug/card/form.vue
  23. 7
      components/ui/rug/card/title.vue
  24. 15
      components/ui/rug/form/action/cancel.vue
  25. 3
      components/ui/rug/form/action/save.vue
  26. 11
      components/ui/rug/form/errors.vue
  27. 28
      components/user/profile/list/item.vue
  28. 77
      config/routes.js
  29. 12
      jsconfig.json
  30. 117
      layouts/default.vue
  31. 31
      layouts/landing.vue
  32. 21
      lib/components.js
  33. 718
      lib/form.js
  34. 102
      lib/routes.js
  35. 47
      lib/store.js
  36. 3
      locales/en.js
  37. 171
      nuxt.config.js
  38. 39
      package.json
  39. 77
      pages/index.vue
  40. 19
      pages/inspire.vue
  41. 37
      pages/landing.vue
  42. 26
      pages/login.vue
  43. 5
      pages/profile/overview.vue
  44. 120
      pages/studies/create.vue
  45. 19
      pages/studies/list.vue
  46. 5
      pages/studies/study/apps/app/edit.vue
  47. 21
      pages/studies/study/apps/index.vue
  48. 51
      pages/studies/study/contributors.vue
  49. 5
      pages/studies/study/contributors/contributor/edit.vue
  50. 93
      pages/studies/study/contributors/contributor/invite.vue
  51. 57
      pages/studies/study/overview.vue
  52. 12
      pages/studies/study/settings.vue
  53. 16
      plugins/axios.js
  54. 10
      store/README.md
  55. 27
      store/apps.js
  56. 76
      store/studies.js
  57. 55
      stubs/studies.js
  58. 30
      test/e2e/index.spec.js
  59. 15
      test/helpers/ava.setup.js
  60. 8
      test/specs/NuxtLogo.spec.js
  61. 4615
      yarn.lock

18
.babelrc

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
{
"env": {
"test": {
"plugins": [
[
"module-resolver",
{
"root": ["."],
"alias": {
"@": ".",
"~": "."
}
}
]
]
}
}
}

24
.eslintrc.js

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: '@babel/eslint-parser',
requireConfigFile: false
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended',
'prettier'
],
plugins: [
],
// add your custom rules here
rules: {
'vue/valid-v-slot': ['error', {
allowModifiers: true,
}],
}
}

2
.nvmrc

@ -1 +1 @@ @@ -1 +1 @@
v14.17.0
16.9.1

4
.prettierrc

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

26
API

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
{
"email": "p.scheltema@rug.nl",
"username": "pscheltema",
"password": "Tralala1!"
}
{
"email": "p.scheltema@rug.nl",
"username": "pscheltema",
"id": 70
}
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYzMjQ3NDg2NywianRpIjoiMTA5NzA0ZmNlZjU3NGJjNzlhNTJhYTA2ZjVmMjE3MDIiLCJ1c2VyX2lkIjo3MH0.VQiSojImYfEwgyp-Ur7Wa-nUd31C2EvmqaJvnYSFYeg",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjMyMzg4NzY3LCJqdGkiOiJmNDhiZjBiNDc2ZmU0NWYwOTFhMzcwNTg0MzgxNGEwNCIsInVzZXJfaWQiOjcwfQ.mAHnQZa_y4eS3QIozC2T2M1U75HGAGvZo9S1k-yrq-M"
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjMyMzg5MTIwLCJqdGkiOiI0OTBmNGU1MTVlYTE0Y2I0YjgxNTViMGZmZjAzOTRmZiIsInVzZXJfaWQiOjcwfQ.ImETvNLdbdOcnWc2Lg4T6PH_L-OonF53EvCgP5IecTU
}

37
BESPREKEN

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
gerelateerde data:
/v1/studies/
- .field
- .image / .src
/v1/studies/$id/
- .field
- .image / .src
/v1/virtualmachines/
- [index].researcher
missende data:
faculty:
/v1/studies/
- .contributors[index].researcher.faculty
apps en researcher
images
profile
image
rest?:
gaan we gerelateerde resources nesten
de api is nu deels bekeken vanuit de auth-user
en deels bekeken vanaf een rest api op machine-to-machine niveau
wat is het simpelste om te doen?
how to:
- wat is de "invite contributor" flow
er moeten denk ik nog api calls bij komen
is er een "pending invitation" contributor placeholder
is dat een union type in "contributors"

55
README.md

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
# vre-frontend
# vre-web
## Build Setup
@ -12,58 +12,5 @@ $ yarn dev @@ -12,58 +12,5 @@ $ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
## Special Directories
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
### `assets`
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
### `components`
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
### `layouts`
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
### `pages`
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
### `plugins`
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
### `static`
This directory contains your static files. Each file inside this directory is mapped to `/`.
Example: `/static/robots.txt` is mapped as `/robots.txt`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
### `store`
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).

64
TODO

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
TODO:
i81n:
routing:
- add locales to generated routes
navigation:
- language navigation
https://lokalise.com/blog/vue-i18n/
https://alecolombo.medium.com/a-simple-multilanguage-site-with-nuxt-js-and-nuxt-i18n-43cce9f9f0fe
auth:
- auth flow with Surfnet
forms:
- use backend responses as form error's
automated deployments:
- validate api interface
get https://api-vre.web.rug.nl/api/swagger/?format=openapi
match used routes with snapshot of implementation
if match && test => deploy
PRIO:
- testrunner => als commit dan run test...
- auth flow
DONE:
navigation:
- create component
routing:
urls:
- remove urls.js in lib folder, and use named routes instead
- typed params XXX NOT POSSIBLE with current templating mechanism, only option is to eject completely...
axios:
- Accept-Language nl_NL en_US
layout:
- setup the "templates"
- create the default layout a la vuetify
https://vuetifyjs.com/en/features/layouts/#usage
https://vuetifyjs.com/en/getting-started/wireframes/#examples
https://vuetifyjs.com/en/components/application/#default-application-markup
i18n:
- redirect to correct language
- setup translations / js/json / $t
- use navigator.language when no language is known
router:
- use router as config
- set navigation as component
- set template with slots dynamic
- set slots content
- get async data
- children can inherit async data

17
UREN

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
Week 1
20/09 9:30 16:05 -0:30 6:05
21/09 9:00 17:00 -0:10 7:20
22/09 9:30 17:00 -0:30 7:00
23/09 9:20 18:00 -0:30 8:10
24/09 9:20 17:00 -0:30 7:10
35:45
Week 2
27/09 9:10 16:00 -0:30 6:40
27/09 17:00 20:00 3:00
28/07 09:45 16:30 -0:15 6:30
29/07 9:00 16:30 -0:15 7:15
30/07 09:00 17:00 -0:45 7:15 TODO
31/07 09:00 17:00 -0:30 7:30 TODO
38:10

BIN
assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

10
ava.config.cjs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
module.exports = () => {
return {
require: ['./test/helpers/ava.setup.js'],
ignoredByWatcher: ['!**/*.{js,vue}'],
babel: true,
tap: true,
verbose: true,
color: true
}
}

11
components/NuxtLogo.vue

@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
<template>
<svg class="nuxt-logo" viewBox="0 0 45 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82" />
</svg>
</template>
<style>
.nuxt-logo {
height: 180px;
}
</style>

46
components/Tutorial.vue

@ -1,46 +0,0 @@ @@ -1,46 +0,0 @@
<!-- Please remove this file from your project -->
<template>
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.1.2/dist/tailwind.min.css" rel="stylesheet">
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
<a class="flex justify-center pt-8 sm:pt-0" href="https://nuxtjs.org" target="_blank">
<svg width="218" height="45" viewBox="0 0 159 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M55.5017 6.81866H60.1727L70.0719 22.9912V6.81866H74.3837V29.7345H69.7446L59.8135 13.5955V29.7345H55.5017V6.81866Z" fill="#003543" /> <path d="M93.657 29.7344H89.6389V27.1747C88.7241 28.9761 86.8628 29.9904 84.5113 29.9904C80.7869 29.9904 78.3684 27.3059 78.3684 23.4423V13.2339H82.3865V22.5976C82.3865 24.8566 83.7594 26.4276 85.8171 26.4276C88.0712 26.4276 89.6389 24.6598 89.6389 22.2377V13.2339H93.657V29.7344Z" fill="#003543" /> <path d="M107.64 29.7344L103.784 24.2342L99.9291 29.7344H95.6492L101.596 21.1242L96.1074 13.2339H100.485L103.784 17.9821L107.051 13.2339H111.461L105.94 21.1242L111.886 29.7344H107.64Z" fill="#003543" /> <path d="M120.053 8.25848V13.2339H124.627V16.6063H120.053V24.7974C120.053 25.0725 120.162 25.3363 120.356 25.531C120.55 25.7257 120.813 25.8353 121.087 25.8357H124.627V29.728H121.98C118.386 29.728 116.035 27.6323 116.035 23.9687V16.6095H112.801V13.2339H114.83C115.776 13.2339 116.327 12.6692 116.327 11.7349V8.25848H120.053Z" fill="#003543" /> <path d="M134.756 24.5446V6.81866H139.066V23.1864C139.066 27.6067 136.943 29.7345 133.349 29.7345H128.332V25.8421H133.461C133.804 25.8421 134.134 25.7054 134.377 25.4621C134.619 25.2188 134.756 24.8888 134.756 24.5446Z" fill="#003543" /> <path d="M141.649 22.0409H145.799C146.029 24.6006 147.728 26.2308 150.472 26.2308C152.923 26.2308 154.623 25.2501 154.623 23.2199C154.623 18.3085 142.331 21.7129 142.331 12.9395C142.334 9.17515 145.568 6.55945 150.215 6.55945C155.05 6.55945 158.317 9.34153 158.516 13.6306H154.388C154.193 11.6341 152.632 10.2918 150.207 10.2918C147.953 10.2918 146.548 11.3397 146.548 12.9427C146.548 18.0173 159 14.2226 159 23.1576C159 27.4131 155.504 30 150.474 30C145.279 30 141.882 26.8563 141.654 22.0441" fill="#003543" /> <path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82" /></svg>
</a>
<div class="mt-8 bg-white overflow-hidden shadow sm:rounded-lg p-6">
<h2 class="text-2xl leading-7 font-semibold">
Welcome to your Nuxt Application
</h2>
<p class="mt-3 text-gray-600">
We recommend you take a look at the <a href="https://nuxtjs.org" target="_blank" class="text-green-500 hover:underline">Nuxt documentation</a>, whether you are new or have previous experience with the framework.<br>
</p>
<p class="mt-4 pt-4 text-gray-800 border-t border-dashed">
To get started, remove <code class="bg-gray-100 text-sm p-1 rounded border">components/Tutorial.vue</code> and start coding in <code class="bg-gray-100 text-sm p-1 rounded border">pages/index.vue</code>. Have fun!
</p>
</div>
<div class="flex justify-center pt-4 space-x-2">
<a href="https://github.com/nuxt/nuxt.js" target="_blank"><svg
class="w-6 h-6 text-gray-600 hover:text-gray-800"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
width="32"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
><path d="M12 2.247a10 10 0 0 0-3.162 19.487c.5.088.687-.212.687-.475c0-.237-.012-1.025-.012-1.862c-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 0 0-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 0 1 1.538 1.025a2.137 2.137 0 0 0 2.912.825a2.104 2.104 0 0 1 .638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 0 1 1.025-2.688a3.594 3.594 0 0 1 .1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 0 1 5 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 0 1 .1 2.65a3.869 3.869 0 0 1 1.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 0 1 .675 1.85c0 1.338-.012 2.413-.012 2.75c0 .263.187.575.687.475A10.005 10.005 0 0 0 12 2.247z" fill="currentColor" /></svg></a>
<a href="https://twitter.com/nuxt_js" target="_blank"><svg
class="w-6 h-6 text-gray-600 hover:text-gray-800"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
width="32"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
><path d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23z" fill="currentColor" /></svg></a>
</div>
</div>
</div>
</template>

22
components/VuetifyLogo.vue

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
<template>
<img
class="VuetifyLogo"
alt="Vuetify Logo"
src="/vuetify-logo.svg"
>
</template>
<style>
.VuetifyLogo {
height: 180px;
width: 180px;
transform: rotateY(560deg);
animation: turn 3.5s ease-out forwards 1s;
}
@keyframes turn {
100% {
transform: rotateY(0deg);
}
}
</style>

48
components/apps/list.vue

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
<template>
<v-container fluid>
<v-row dense>
<v-col
v-for="app in apps"
:key="app.id"
cols="6"
>
<v-card link :to="{ name: 'researchStudies.study.apps.edit', params: {studyId: studyId, appId: app.id } }">
<v-img
:src="`https://picsum.photos/seed/${app.id}/50/50`"
height="50"
width="50"
/>
<v-card-title>{{app.profile_name}}</v-card-title>
<v-card-subtitle v-text="app.operating_system_name"></v-card-subtitle>
<v-card-text>
Name: {{ app.name }}
<br/>
Researcher: {{ app.researcher }}
<br/>
<br/>
Created at: {{ $d(new Date(app.created_at), 'long') }}
<br/>
Updated at: {{ $d(new Date(app.updated_at), 'long') }}
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
props: {
apps: {
type: Array,
default() {return []},
},
studyId: {
type: String,
default: '',
}
}
}
</script>

39
components/contributor/list/item.vue

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<template>
<v-list-item class="px-2" link :to="{ name: 'researchStudies.study.contributors.edit', params: { studyId: studyId, contributorId: contributor.researcher.id } }">
<v-list-item-avatar>
<v-img :src="`https://randomuser.me/api/portraits/men/${contributor.researcher.id+6}.jpg`"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="text-h6">
{{contributor.researcher.first_name}} {{contributor.researcher.last_name}}
</v-list-item-title>
<v-list-item-subtitle>{{contributor.researcher.email_address}}</v-list-item-subtitle>
<v-list-item-subtitle>Factuly: {{contributor.researcher.faculty}}</v-list-item-subtitle>
<v-list-item-subtitle>Role: {{contributor.role}}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
<script>
export default {
props: {
studyId: {
type: [String, Number],
default: '',
},
contributor: {
type: Object,
default: () => ({
researcher: {
id: '',
first_name: '',
last_name: '',
email_address: '',
faculty: '',
},
role: '',
})
}
},
}
</script>

97
components/contributors/editForm.vue

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
<template>
<v-card>
<v-card-title>
<span class="text-h5">{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col
cols="12"
sm="6"
md="4"
>
<v-select
v-model="editContributor.roles"
:items="roles"
item-text="name"
item-value="id"
multiple
chips
label="study field"
></v-select>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue darken-1"
text
@click="close"
>
Cancel
</v-btn>
<v-btn
color="blue darken-1"
text
@click="save"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: {
defaultContributor: {
type: Object,
default: () => { return {}; },
},
item: {
type: Object,
default: () => { return {}; },
},
isNew: {
type: Boolean,
default: true,
},
roles: {
type: Array,
default: () => [],
},
apps: {
type: Array,
default: () => [],
},
onClose: {
type: Function,
default: () => {},
},
},
data() {
return {
editContributor: this.item,
}
},
computed: {
formTitle () {
return this.editedIndex === -1 ? 'New Item' : 'Edit Item'
},
},
methods: {
close() {
this.onClose();
},
save() {
},
}
}
</script>

8
components/logo/rug.vue

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
<template>
<router-link :to="{ name: 'landing' }">
<img
height="50"
:src="require('~/assets/logo.png')" alt="RUG"
/>
</router-link>
</template>

80
components/navigation.vue

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
<template>
<div>
<v-list>
<UserProfileListItem :user="user" />
</v-list>
<v-divider></v-divider>
<v-list>
<v-list-item link :to="{ name: 'researchStudies' }">
<v-list-item-icon>
<v-icon>mdi-view-list</v-icon>
</v-list-item-icon>
<v-list-item-title>Research studies</v-list-item-title>
</v-list-item>
</v-list>
<v-divider v-if="showStudyDetailNavigation"></v-divider>
<v-list-group
v-if="showStudyDetailNavigation"
:value="true"
>
<template #activator>
<v-list-item-content>
<v-list-item-title>{{ getActiveStudy.name }}</v-list-item-title>
</v-list-item-content>
</template>
<v-list-item link :to="{ name: 'researchStudies.study.overview', params: { studyId: $route.params.studyId } }">
<v-list-item-icon>
<v-icon>mdi-file-document</v-icon>
</v-list-item-icon>
<v-list-item-title>Overview</v-list-item-title>
</v-list-item>
<v-list-item link :to="{ name: 'researchStudies.study.contributors', params: { studyId: $route.params.studyId } }">
<v-list-item-icon>
<v-icon>mdi-account-group</v-icon>
</v-list-item-icon>
<v-list-item-title>Contributors</v-list-item-title>
</v-list-item>
<v-list-item link :to="{ name: 'researchStudies.study.apps', params: { studyId: $route.params.studyId } }">
<v-list-item-icon>
<v-icon>mdi-desktop-mac</v-icon>
</v-list-item-icon>
<v-list-item-title>Apps</v-list-item-title>
</v-list-item>
<v-list-item link :to="{ name: 'researchStudies.study.settings', params: { studyId: $route.params.studyId } }">
<v-list-item-icon>
<v-icon>mdi-cog</v-icon>
</v-list-item-icon>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item>
</v-list-group>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
user: this.$auth.user,
}
},
computed: {
showStudyDetailNavigation() {
return this.$route.params?.studyId;
},
...mapGetters('studies', [
'getActiveStudy',
]),
},
}
</script>

8
components/researchStudies/createButton.vue

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
<template>
<v-btn
:to="{ name: 'researchStudies.create' }"
>
<v-icon>mdi-plus</v-icon>
Create
</v-btn>
</template>

90
components/researchStudies/list.vue

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
<template>
<v-card>
<v-card-title>
Research Studies
<v-spacer></v-spacer>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="Search"
single-line
hide-details
></v-text-field>
</v-card-title>
<v-data-table
multi-sort
hide-default-footer
:headers="headers"
:items="studies"
:search="search"
@click:row="navigateToRow"
>
<template #item.image="{ item }">
<v-avatar>
<v-img
:src="item.src"
:alt="item.name"
/>
</v-avatar>
</template>
<template #item.startDate="{ item }">
{{$d(item.startDate, 'short')}}
</template>
<template #item.isShared="{ item }">
<v-icon v-if="item.isShared" icon-account-group></v-icon>
</template>
</v-data-table>
</v-card>
</template>
<script>
export default {
props: {
studies: {
type: Array,
default() {
return []
},
},
},
data () {
return {
search: '',
headers: [
{
text: '',
align: 'start',
value: 'image',
},
{
text: 'Name',
value: 'name'
},
{
text: 'Field',
value: 'field',
},
{
text: 'Start Date',
value: 'startDate'
},
{
text: '',
sortable: false,
value: 'isShared'
}
],
}
},
methods: {
navigateToRow(row) {
this.$router.push({
name: 'researchStudies.study.overview',
params: {
studyId: row.id
}
});
}
},
}
</script>

40
components/ui/rug/card/form.vue

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
<template>
<form @submit.prevent="form.onSubmit">
<v-card :loading="form.isLoading">
<slot />
<ui-rug-form-errors v-if="showFormErrors && form.error">
{{ form.error }}
</ui-rug-form-errors>
<v-divider v-if="useActions"></v-divider>
<v-card-actions v-if="useActions">
<v-spacer />
<ui-rug-form-action-cancel />
<ui-rug-form-action-save />
</v-card-actions>
</v-card>
</form>
</template>
<script>
export default {
props: {
form: {
type: Object,
default: () => ({})
},
showFormErrors: {
type: Boolean,
default: true
},
useActions: {
type: Boolean,
default: true
}
}
};
</script>

7
components/ui/rug/card/title.vue

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
<script>
import { createSimpleFunctional } from '@/lib/components';
const Title = createSimpleFunctional('v-card__title', 'blue');
export default Title;
</script>

15
components/ui/rug/form/action/cancel.vue

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
<template>
<v-btn @click="onCancel">
cancel
</v-btn>
</template>
<script>
export default {
methods: {
onCancel() {
this.$router.go(-1);
}
}
}
</script>

3
components/ui/rug/form/action/save.vue

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
<template>
<v-btn type="submit">Save</v-btn>
</template>

11
components/ui/rug/form/errors.vue

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
<template>
<v-alert
text
dense
type="error"
border="left"
class="ma-4"
>
<slot />
</v-alert>
</template>

28
components/user/profile/list/item.vue

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
<template>
<v-list-item class="px-2" link :to="{ name: 'profile.overview' }">
<v-list-item-avatar>
<v-img :src="`https://randomuser.me/api/portraits/men/${user.id+6}.jpg`"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="text-h6">
{{user.name}}
</v-list-item-title>
<v-list-item-subtitle>{{user.email}}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
<script>
export default {
props: {
user: {
type: Object,
default: () => ({
id: '',
name: '',
email: ''
})
}
},
}
</script>

77
config/routes.js

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
// https://router.vuejs.org/api/#router-construction-options
/**
*
* translations
* TODO
*
*/
export default {
routes: [
{
name: 'landing',
path: '/',
component: 'pages/landing',
},
{ // TEMP untill route translations are done
name: 'landing.en',
path: '/en/',
component: 'pages/landing',
},
{
name: 'login',
path: '/login/',
component: 'pages/login',
},
{
name: 'profile.overview',
path: '/en/profile/',
component: 'pages/profile/overview',
},
{
name: 'researchStudies',
path: '/en/researchStudies/',
component: 'pages/studies/list',
},
{
name: 'researchStudies.create',
path: '/en/researchStudies/create',
component: 'pages/studies/create',
},
{
name: 'researchStudies.study.overview',
path: '/en/researchStudies/:studyId/overview/',
component: 'pages/studies/study/overview',
},
{
name: 'researchStudies.study.contributors',
path: '/en/researchStudies/:studyId/contributors/',
component: 'pages/studies/study/contributors',
},
{
name: 'researchStudies.study.contributors.invite',
path: '/en/researchStudies/:studyId/contributors/invite/',
component: 'pages/studies/study/contributors/contributor/invite',
},
{
name: 'researchStudies.study.contributors.edit',
path: '/en/researchStudies/:studyId/contributors/:contributorId/edit/',
component: 'pages/studies/study/contributors/contributor/edit',
},
{
name: 'researchStudies.study.apps',
path: '/en/researchStudies/:studyId/apps/',
component: 'pages/studies/study/apps',
},
{
name: 'researchStudies.study.apps.edit',
path: '/en/researchStudies/:studyId/apps/:appId/edit',
component: 'pages/studies/study/apps/app/edit',
},
{
name: 'researchStudies.study.settings',
path: '/en/researchStudies/:studyId/settings/',
component: 'pages/studies/study/settings',
},
],
}

12
jsconfig.json

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./*"],
"~~/*": ["./*"],
"@@/*": ["./*"]
}
},
"exclude": ["node_modules", ".nuxt", "dist"]
}

117
layouts/default.vue

@ -1,117 +1,28 @@ @@ -1,117 +1,28 @@
<template>
<v-app dark>
<v-navigation-drawer
v-model="drawer"
:mini-variant="miniVariant"
:clipped="clipped"
fixed
<v-app>
<v-navigation-drawer
app
permanent
expand-on-hover
clipped
>
<v-list>
<v-list-item
v-for="(item, i) in items"
:key="i"
:to="item.to"
router
exact
>
<v-list-item-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title v-text="item.title" />
</v-list-item-content>
</v-list-item>
</v-list>
<Navigation />
</v-navigation-drawer>
<v-app-bar
:clipped-left="clipped"
fixed
<v-app-bar
app
clipped-left
>
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
<v-btn
icon
@click.stop="miniVariant = !miniVariant"
>
<v-icon>mdi-{{ `chevron-${miniVariant ? 'right' : 'left'}` }}</v-icon>
</v-btn>
<v-btn
icon
@click.stop="clipped = !clipped"
>
<v-icon>mdi-application</v-icon>
</v-btn>
<v-btn
icon
@click.stop="fixed = !fixed"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
<v-toolbar-title v-text="title" />
<v-spacer />
<v-btn
icon
@click.stop="rightDrawer = !rightDrawer"
>
<v-icon>mdi-menu</v-icon>
</v-btn>
<LogoRug />
<router-view name="header"></router-view>
</v-app-bar>
<v-main>
<v-container>
<Nuxt />
</v-container>
</v-main>
<v-navigation-drawer
v-model="rightDrawer"
:right="right"
temporary
fixed
>
<v-list>
<v-list-item @click.native="right = !right">
<v-list-item-action>
<v-icon light>
mdi-repeat
</v-icon>
</v-list-item-action>
<v-list-item-title>Switch drawer (click me)</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-footer
:absolute="!fixed"
app
>
<span>&copy; {{ new Date().getFullYear() }}</span>
</v-footer>
</v-app>
</template>
<script>
export default {
data () {
return {
clipped: false,
drawer: false,
fixed: false,
items: [
{
icon: 'mdi-apps',
title: 'Welcome',
to: '/'
},
{
icon: 'mdi-chart-bubble',
title: 'Inspire',
to: '/inspire'
}
],
miniVariant: false,
right: true,
rightDrawer: false,
title: 'Vuetify.js'
}
}
}
</script>

31
layouts/landing.vue

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<template>
<v-app>
<v-app-bar
app
clipped-left
>
<LogoRug />
<router-view name="header"></router-view>
</v-app-bar>
<v-main>
<v-container>
<Nuxt />
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
data () {
return {
title: 'landing layout'
}
}
}
</script>

21
lib/components.js

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
import Vue from 'vue'
// copied from 'vuetify/util/helpers'; and added classesAsString param
export function createSimpleFunctional(
c,
classesAsString,
el = 'div',
name
) {
return Vue.extend({
name: name || c.replace(/__/g, '-'),
functional: true,
render (h, { data, children }) {
data.staticClass = (`${c} ${classesAsString} ${data.staticClass || ''}`).trim()
return h(el, data, children)
},
})
}

718
lib/form.js

@ -0,0 +1,718 @@ @@ -0,0 +1,718 @@
import Vue from 'vue';
import _ from 'lodash';
import Ajv from 'ajv';
import localizeEn from 'ajv-i18n/localize/en';
// import localize_nl from 'ajv-i18n/localize/nl';
// wrapped in _.once so it is only executed once
// const setupMessages = _.once((vm) => {
// Not really sure why, but at server render time and client render time there seems to be a difference in
// loading order. If I try to use vm.$t directly it errors on the server in some situations and on the client
// in others. This mixing works consistently
// const $t = Vue.prototype.$t.bind(vm);
// const $t = (identity) => identity;
// setup messages
// Validator.setMessages('nl', {
// accepted: $t('form.validation.accepted'),
// after: $t('form.validation.after'),
// after_or_equal: $t('form.validation.after-or-equal'),
// alpha: $t('form.validation.alpha'),
// alpha_dash: $t('form.validation.alpha-dash'),
// alpha_num: $t('form.validation.alpha-num'),
// before: $t('form.validation.before'),
// before_or_equal: $t('form.validation.before-or-equal'),
// between: $t('form.validation.between'),
// confirmed: $t('form.validation.confirmed'),
// email: $t('form.validation.email'),
// date: $t('form.validation.date'),
// def: $t('form.validation.def'),
// digits: $t('form.validation.digits'),
// different: $t('form.validation.different'),
// 'in': $t('form.validation.in'),
// integer: $t('form.validation.integer'),
// hex: $t('form.validation.hex'),
// min: {
// numeric: $t('form.validation.min.numeric'),
// string: $t('form.validation.min.string'),
// },
// max: {
// numeric: $t('form.validation.max.numeric'),
// string: $t('form.validation.max.string'),
// },
// not_in: $t('form.validation.not-in'),
// numeric: $t('form.validation.numeric'),
// present: $t('form.validation.present'),
// required: $t('form.validation.required'),
// required_if: $t('form.validation.required-if'),
// required_unless: $t('form.validation.required-unless'),
// required_with: $t('form.validation.required-with'),
// required_with_all: $t('form.validation.required-with-all'),
// required_without: $t('form.validation.required-without'),
// required_without_all: $t('form.validation.required-without-all'),
// same: $t('form.validation.same'),
// size: {
// numeric: $t('form.validation.size.numeric'),
// string: $t('form.validation.size.string'),
// },
// string: $t('form.validation.string'),
// url: $t('form.validation.url'),
// regex: $t('form.validation.regex'),
// attributes: {
// 'address': $t('form.validation.attributes.address'), //'adres',
// 'age': $t('form.validation.attributes.age'), //'leeftijd',
// 'available': $t('form.validation.attributes.available'), //'beschikbaar',
// 'city': $t('form.validation.attributes.city'), //'stad',
// 'content': $t('form.validation.attributes.content'), //'inhoud',
// 'country': $t('form.validation.attributes.country'), //'land',
// 'date': $t('form.validation.attributes.date'), //'datum',
// 'day': $t('form.validation.attributes.day'), //'dag',
// 'description': $t('form.validation.attributes.description'), //'omschrijving',
// 'email': $t('form.validation.attributes.email'), //'e-mailadres',
// 'excerpt': $t('form.validation.attributes.excerpt'), //'uittreksel',
// 'firstName': $t('form.validation.attributes.firstName'), //'voornaam',
// 'gender': $t('form.validation.attributes.gender'), //'geslacht',
// 'hour': $t('form.validation.attributes.hour'), //'uur',
// 'lastName': $t('form.validation.attributes.lastName'), //'achternaam',
// 'message': $t('form.validation.attributes.message'), //'boodschap',
// 'minute': $t('form.validation.attributes.minute'), //'minuut',
// 'mobile': $t('form.validation.attributes.mobile'), //'mobiel',
// 'month': $t('form.validation.attributes.month'), //'maand',
// 'name': $t('form.validation.attributes.name'), //'naam',
// 'password': $t('form.validation.attributes.password'), //'wachtwoord',
// 'passwordConfirmation': $t('form.validation.attributes.passwordConfirmation'), //'wachtwoordbevestiging',
// 'phone': $t('form.validation.attributes.phone'), //'telefoonnummer',
// 'second': $t('form.validation.attributes.second'), //'seconde',
// 'sex': $t('form.validation.attributes.sex'), //'geslacht',
// 'size': $t('form.validation.attributes.size'), //'grootte',
// 'subject': $t('form.validation.attributes.subject'), //'onderwerp',
// 'time': $t('form.validation.attributes.time'), //'tijd',
// 'title': $t('form.validation.attributes.title'), //'titel',
// 'username': $t('form.validation.attributes.username'), //'gebruikersnaam',
// 'year': $t('form.validation.attributes.year'), //'jaar',
// 'dateFrom': $t('form.validation.attributes.dateFrom'), //'begin datum',
// 'dateTo': $t('form.validation.attributes.dateTo'), //'eind datum',
// 'organizer': $t('form.validation.attributes.organizer'), //'organisator',
// 'enrollmentLink': $t('form.validation.attributes.enrollmentLink'), //'inschrijvingslink',
// 'enrollmentDeadline': $t('form.validation.attributes.enrollmentDeadline'), //'inschrijvingsdeadline',
// 'price': $t('form.validation.attributes.price'), //'prijs',
// 'location': $t('form.validation.attributes.location'), //'locatie',
// 'moreInfo': $t('form.validation.attributes.moreInfo'), //'meer info',
// 'deadline': $t('form.validation.attributes.deadline'), //'deadline',
// 'rules': $t('form.validation.attributes.rules'), //'reglement',
// 'organization': $t('form.validation.attributes.organization'), //'organisatie',
// 'reward': $t('form.validation.attributes.reward'), //'prijs',
// 'type': $t('form.validation.attributes.type'), //'type',
// 'contributionsPerUser': $t('form.validation.attributes.contributionsPerUser'), //'inzendingen per deelnemer',
// 'useAgeCategories': $t('form.validation.attributes.useAgeCategories'), //'gebruik leeftijdscategoriën',
// 'showJuryFeedbackToParticipant': $t('form.validation.attributes.showJuryFeedbackToParticipant'), //'toon feedback aan deelnemers',
// 'extraField_1': $t('form.validation.attributes.extraField_1'), //'extra vraag 1',
// 'extraField_2': $t('form.validation.attributes.extraField_2'), //'extra vraag 2',
// 'ageCategories': $t('form.validation.attributes.ageCategories'), //'leeftijdscategoriën',
// 'ageFrom': $t('form.validation.attributes.ageFrom'), //'leeftijd vanaf',
// 'ageTo': $t('form.validation.attributes.ageTo'), //'leeftijd tot',
// 'id': $t('form.validation.attributes.id'), //'id',
// 'body': $t('form.validation.attributes.body'), //'omschrijving',
// 'private': $t('form.validation.attributes.private'), //'prive',
// 'oldPassword': $t('form.validation.attributes.oldPassword'), //'huidig wachtwoord',
// 'termsVersion': $t('form.validation.attributes.termsVersion'), //'versie van de voorwaarden',
// 'nickname': $t('form.validation.attributes.nickname'), //'pseudoniem',
// 'dateOfBirth': $t('form.validation.attributes.dateOfBirth'), //'geboortedatum',
// 'addressStreet': $t('form.validation.attributes.addressStreet'), //'straat',
// 'addressNumber': $t('form.validation.attributes.addressNumber'), //'nummer',
// 'addressPostalCode': $t('form.validation.attributes.addressPostalCode'), //'postcode',
// 'addressCity': $t('form.validation.attributes.addressCity'), //'gemeente',
// 'addressCountry': $t('form.validation.attributes.addressCountry'), //'land',
// 'website': $t('form.validation.attributes.website'), //'website',
// 'twitter': $t('form.validation.attributes.twitter'), //'twitter',
// 'education': $t('form.validation.attributes.education'), //'opleiding',
// 'publications': $t('form.validation.attributes.publications'), //'publicaties',
// 'prizes': $t('form.validation.attributes.prizes'), //'prijzen',
// 'receive_only_crucial_email': $t('form.validation.attributes.receive_only_crucial_email'), //'ontvang enkel cruciale e-mails',
// },
// });
// use this language
// Validator.useLang('nl');
// add validators
// checks for ['image/png', 'image/jpg', 'image/jpeg']
// const imageRe = /image\/(png|jpg|jpeg)/i;
// Validator.register('base64image', function(value) {
// if (!value.data) return true;
// return value.data.match(imageRe);
// }, $t('form.validation.base64image'));
// Validator.register('requiredImage', function(value) {
// return value.data || value.id;
// }, $t('form.validation.requiredImage'));
// // checks for max-file-size
// Validator.register('max_image', function(value, requirement) {
// if (!value.file) return true;
// return value.file && (value.file.size / 1024 / 1024) < parseInt(requirement);
// // return true;
// }, $t('form.validation.max-image'));
// // requires value to be truthy
// Validator.register('true', function(value) {
// return !!value;
// }, $t('form.validation.true'));
// // requires value to be comma-seperated list of emails
// Validator.register('emails', function(value) {
// const pattern = /^([\t\s\n]*(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))[\t\s\n]*,?)*$/; // eslint-disable-line
// return pattern.test(value);
// }, $t('form.validation.emails'));
// // requires an array to have minimum items
// Validator.register('min_items', function(value, requirement) {
// if (!value || !value.length) return true;
// return value.length >= parseInt(requirement);
// }, $t('form.validation.min-items'));
// // requires an array to have maximum items
// Validator.register('max_items', function(value, requirement) {
// if (!value) return true;
// return value.length <= parseInt(requirement);
// }, $t('form.validation.max-items'));
// // requires nothing
// Validator.register('', function() {
// return true;
// }, $t('form.validation.okay'));
// });
/**
* NOTE:
* event.target.dataPath or event.target.id is used in that order to locate the element in the form
*/
/**
* EXAMPLE:
* data: function() {
return {
formData: {
email: 'paulschelte',
password: '',
},
form: new Form({
form: 'formData',
vm: this,
method: 'post',
url: '/api/auth/login/',
schema: {
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
},
password: {
type: 'string',
minLength: 6,
},
},
required: ['email', 'password'],
},
onResponse: this.doLogin,
}),
};
},
methods: {
doLogin(data) {
// do something with data
}
}
*
* <form @submit.prevent="form.onSubmit">
* <TextInput
* id="formData.email"
* v-model="formData.email"
* :error="form.getError('formData.email')"