Переглянути джерело

another pre-release (#53)

tags/0.0.3
maeda 3 роки тому
джерело
коміт
b0c2120e96
86 змінених файлів з 2469 додано та 9083 видалено
  1. 27
    1
      .drone.yml
  2. 22
    8
      backend/.env.sample
  3. 1
    0
      backend/db/data-generator/config.json
  4. 39
    12
      backend/db/data-generator/mock.js
  5. 2
    2
      backend/db/dataSort.js
  6. 1
    1
      backend/db/migrations/20220403111037_create_tag_associations_table.js
  7. 6
    6
      backend/db/seeds/02-profiles.js
  8. 11
    6
      backend/db/seeds/04-responses.js
  9. 5
    0
      backend/knexfile.js
  10. 2
    0
      backend/lib/plugins/membership.js
  11. 4
    0
      backend/lib/plugins/profile.js
  12. 0
    1
      backend/lib/routes/membership/active.js
  13. 104
    0
      backend/lib/routes/membership/reveal.js
  14. 87
    0
      backend/lib/routes/tag/get.js
  15. 91
    0
      backend/lib/routes/tag/reveal.js
  16. 10
    3
      backend/lib/schemas/params.js
  17. 1
    1
      backend/lib/schemas/tags.js
  18. 33
    1
      backend/lib/services/profile/index.js
  19. 1
    1
      backend/lib/services/profile/profiler.js
  20. 130
    8357
      backend/package-lock.json
  21. 2
    1
      backend/package.json
  22. 1
    0
      backend/server/index.js
  23. 41
    7
      backend/server/manifest.js
  24. 5
    5
      deployment/post-receive
  25. 1
    0
      frontend/assets/icons/calendar.svg
  26. 1
    0
      frontend/assets/icons/chat.svg
  27. 1
    0
      frontend/assets/icons/close-icon.svg
  28. 1
    0
      frontend/assets/icons/options-icon.svg
  29. BIN
      frontend/assets/logos/siimee_logo.jpg
  30. 11
    0
      frontend/package-lock.json
  31. 1
    0
      frontend/package.json
  32. 12
    12
      frontend/src/App.vue
  33. 19
    2
      frontend/src/components/AspectBar.vue
  34. 29
    0
      frontend/src/components/ChatBubble.vue
  35. 41
    0
      frontend/src/components/DynamicTagList.vue
  36. 1
    1
      frontend/src/components/Messages.vue
  37. 36
    10
      frontend/src/components/NamePlate.vue
  38. 44
    6
      frontend/src/components/PairingButton.vue
  39. 39
    20
      frontend/src/components/PairsList.vue
  40. 56
    23
      frontend/src/components/ProfileCard.vue
  41. 9
    85
      frontend/src/components/ProfileCardList.vue
  42. 91
    0
      frontend/src/components/SpiderChart.vue
  43. 61
    14
      frontend/src/components/SummaryBar.vue
  44. 23
    7
      frontend/src/components/TagList.vue
  45. 42
    0
      frontend/src/components/onboarding/AccountType.vue
  46. 48
    0
      frontend/src/components/onboarding/Aspects.vue
  47. 40
    0
      frontend/src/components/onboarding/CompanyID.vue
  48. 41
    0
      frontend/src/components/onboarding/FormDropdown.vue
  49. 36
    0
      frontend/src/components/onboarding/FormInput.vue
  50. 36
    0
      frontend/src/components/onboarding/FormTags.vue
  51. 41
    0
      frontend/src/components/onboarding/Interests.vue
  52. 44
    0
      frontend/src/components/onboarding/LicensesAndCertifications.vue
  53. 40
    0
      frontend/src/components/onboarding/Location.vue
  54. 4
    5
      frontend/src/components/onboarding/QuestionResponse.vue
  55. 40
    0
      frontend/src/components/onboarding/Role.vue
  56. 41
    0
      frontend/src/components/onboarding/Skills.vue
  57. 42
    0
      frontend/src/components/onboarding/Splash.vue
  58. 27
    0
      frontend/src/components/onboarding/index.js
  59. 36
    33
      frontend/src/entities/card/card.js
  60. 43
    0
      frontend/src/entities/grouping/grouping.js
  61. 4
    0
      frontend/src/entities/grouping/grouping.schema.js
  62. 1
    1
      frontend/src/entities/profile/profile.js
  63. 28
    10
      frontend/src/entities/survey/survey.js
  64. 2
    1
      frontend/src/main.js
  65. 20
    8
      frontend/src/router/guards.js
  66. 12
    0
      frontend/src/router/index.js
  67. 30
    4
      frontend/src/services/chat.service.js
  68. 35
    10
      frontend/src/services/grouping.service.js
  69. 8
    9
      frontend/src/services/index.js
  70. 46
    13
      frontend/src/services/login.service.js
  71. 1
    1
      frontend/src/services/notification.service.js
  72. 24
    4
      frontend/src/services/profile.service.js
  73. 14
    8
      frontend/src/services/queue.service.js
  74. 13
    7
      frontend/src/services/survey.service.js
  75. 2
    2
      frontend/src/services/user.service.js
  76. 30
    32
      frontend/src/utils/db.js
  77. 64
    35
      frontend/src/utils/index.js
  78. 23
    55
      frontend/src/utils/lang.js
  79. 12
    19
      frontend/src/utils/mixins.js
  80. 68
    18
      frontend/src/utils/survey.js
  81. 55
    27
      frontend/src/views/ChatView.vue
  82. 28
    83
      frontend/src/views/HomeView.vue
  83. 7
    19
      frontend/src/views/MessagesView.vue
  84. 139
    52
      frontend/src/views/OnboardingView.vue
  85. 41
    34
      frontend/src/views/PairsView.vue
  86. 58
    0
      frontend/tests/login.service.spec.js

+ 27
- 1
.drone.yml Переглянути файл

@@ -55,7 +55,6 @@ type: docker
55 55
 name: frontend_run_build
56 56
 depends_on:
57 57
     - frontend_run_tests
58
-    - backend_run_tests
59 58
 trigger:
60 59
     status:
61 60
         - success
@@ -86,3 +85,30 @@ volumes:
86 85
     - name: frontend_build
87 86
       host:
88 87
           path: /tmp/cache/drone/frontend/build
88
+---
89
+#####################
90
+# Deploy to Staging #
91
+#####################
92
+kind: pipeline
93
+name: deploy
94
+trigger:
95
+    branch:
96
+        - dev
97
+
98
+steps:
99
+    # post-receive hook
100
+    - name: push commit
101
+      image: appleboy/drone-git-push:0.2.0-linux-amd64
102
+      settings:
103
+          branch: master
104
+          remote: maeda@165.232.128.85:/opt/staging/siimee.git
105
+          remote_name: staging
106
+          force: true
107
+          ssh_key:
108
+              # !: id_rsa from DRONE machine
109
+              from_secret: push_deploy_key
110
+
111
+volumes:
112
+    - name: backend_node_cache
113
+      host:
114
+          path: /tmp/cache/drone/backend/node_modules

+ 22
- 8
backend/.env.sample Переглянути файл

@@ -1,20 +1,34 @@
1 1
 # Rename me to .env then fill me with runtime configuration and credentials
2 2
 # Just don't try to check me into your repo :)
3 3
 # Confused? See https://github.com/motdotla/dotenv
4
-#
5
-# e.g.
4
+
5
+# API host:port
6 6
 API_HOST=localhost
7 7
 API_PORT=3001
8
-APP_SECRET=mysecret
9 8
 
10 9
 USE_LOCAL_DB=true
10
+DB_TYPE=mysql
11 11
 
12
-DB_ROOT_PASSWORD=root
13
-DB_USER=root
14
-DB_NAME=test
12
+
13
+# Extra pepper for auth encryption
14
+PEPPER=kosho
15
+APP_SECRET=mysecret
16
+
17
+
18
+# Config for local test dB
15 19
 DB_HOST=localhost
16
-DB_TYPE=mysql
17 20
 DB_PORT=3307
21
+DB_NAME=test
22
+
23
+DB_USER=root
24
+DB_ROOT_PASSWORD=root
18 25
 
26
+
27
+# Config for remote planet scale production dB
28
+PSCALE_DB_HOST=planet-scale-db
29
+PSCALE_DB_PORT=3306
30
+PSCALE_DB_NAME=pscale-db
19 31
 PSCALE_DB_BRANCH=main
20
-PSCALE_DB_NAME=planet-scale-db
32
+
33
+PSCALE_DB_USER=myuserpleasechange
34
+PSCALE_DB_PASSWORD=pscale_pw_abc123efg456hij789

+ 1
- 0
backend/db/data-generator/config.json Переглянути файл

@@ -2,6 +2,7 @@
2 2
     "mockOutputPath": "./db/generated",
3 3
     "magic": 1000,
4 4
     "total": 100,
5
+    "ignore": [45],
5 6
     "batchSize": 10,
6 7
     "percentageOfSeekers": 90,
7 8
     "scoreVals": [1, 2, 3, 4, 5, 6, 7],

+ 39
- 12
backend/db/data-generator/mock.js Переглянути файл

@@ -38,54 +38,80 @@ module.exports = {
38 38
             tag_description: 'urgency',
39 39
             is_active: true,
40 40
         },
41
+        {
42
+            tag_id: 7,
43
+            tag_category: 'reveal',
44
+            tag_description: 'user_name',
45
+            is_active: true,
46
+        },
47
+        {
48
+            tag_id: 8,
49
+            tag_category: 'reveal',
50
+            tag_description: 'user_email',
51
+            is_active: true,
52
+        },
41 53
     ],
42 54
     tag_associations: [
43 55
         {
44 56
             tag_association_id: 1,
45 57
             profile_id: 1,
46
-            membership_id: null,
58
+            grouping_id: null,
47 59
             tag_id: 1,
48 60
             is_deleted: false,
49 61
         },
50 62
         {
51 63
             tag_association_id: 2,
52 64
             profile_id: 2,
53
-            membership_id: null,
65
+            grouping_id: null,
54 66
             tag_id: 1,
55 67
             is_deleted: false,
56 68
         },
57 69
         {
58 70
             tag_association_id: 3,
59 71
             profile_id: 3,
60
-            membership_id: null,
72
+            grouping_id: null,
61 73
             tag_id: 1,
62 74
             is_deleted: false,
63 75
         },
64 76
         {
65 77
             tag_association_id: 4,
66 78
             profile_id: 45,
67
-            membership_id: null,
79
+            grouping_id: null,
68 80
             tag_id: 1,
69 81
             is_deleted: false,
70 82
         },
71 83
         {
72 84
             tag_association_id: 5,
73 85
             profile_id: 45,
74
-            membership_id: 1,
86
+            grouping_id: 2,
75 87
             tag_id: 4,
76 88
             is_deleted: false,
77 89
         },
78 90
         {
79 91
             tag_association_id: 6,
80 92
             profile_id: 45,
81
-            membership_id: 1,
93
+            grouping_id: 2,
82 94
             tag_id: 5,
83 95
             is_deleted: false,
84 96
         },
85 97
         {
86 98
             tag_association_id: 7,
87 99
             profile_id: 2,
88
-            membership_id: 1,
100
+            grouping_id: 2,
101
+            tag_id: 5,
102
+            is_deleted: false,
103
+        },
104
+        {
105
+            tag_association_id: 8,
106
+            profile_id: 41,
107
+            grouping_id: 1,
108
+            tag_id: 4,
109
+            is_deleted: false,
110
+        },
111
+        {
112
+            tag_association_id: 9,
113
+            profile_id: 41,
114
+            grouping_id: 1,
89 115
             tag_id: 5,
90 116
             is_deleted: false,
91 117
         },
@@ -94,7 +120,8 @@ module.exports = {
94 120
         {
95 121
             response_key_id: 1,
96 122
             response_key_category: 'visionary_vs_implementer',
97
-            response_key_prompt: 'While managing your team, do you find success in your employees being more Visionary or Implementer?',
123
+            response_key_prompt:
124
+                'While managing your team, do you find success in your employees being more Visionary or Implementer?',
98 125
             response_key_description: 'first round draft scoring question',
99 126
         },
100 127
         {
@@ -225,12 +252,12 @@ module.exports = {
225 252
     ],
226 253
     messages: [],
227 254
     match_queues: [
228
-        { match_queue_id: 1, profile_id: 45, target_id: 62, is_deleted: false },
229
-        { match_queue_id: 2, profile_id: 62, target_id: 45, is_deleted: false },
255
+        { match_queue_id: 1, profile_id: 47, target_id: 62, is_deleted: false },
256
+        { match_queue_id: 2, profile_id: 62, target_id: 47, is_deleted: false },
230 257
         { match_queue_id: 3, profile_id: 62, target_id: 46, is_deleted: false },
231 258
         { match_queue_id: 4, profile_id: 46, target_id: 62, is_deleted: false },
232
-        { match_queue_id: 5, profile_id: 45, target_id: 46, is_deleted: false },
233
-        { match_queue_id: 6, profile_id: 46, target_id: 45, is_deleted: false },
259
+        { match_queue_id: 5, profile_id: 47, target_id: 46, is_deleted: false },
260
+        { match_queue_id: 6, profile_id: 46, target_id: 48, is_deleted: false },
234 261
         { match_queue_id: 7, profile_id: 46, target_id: 44, is_deleted: false },
235 262
         { match_queue_id: 8, profile_id: 46, target_id: 43, is_deleted: false },
236 263
         { match_queue_id: 9, profile_id: 46, target_id: 42, is_deleted: false },

+ 2
- 2
backend/db/dataSort.js Переглянути файл

@@ -1,3 +1,3 @@
1
-const dataSort = (table, id) => { return table.sort((a,b) => {return a[`${id}`] < b[`${id}`]})}
1
+const dataSort = (table, id) => table.sort((a, b) => a[`${id}`] < b[`${id}`])
2 2
 
3
-module.exports = dataSort
3
+module.exports = dataSort

+ 1
- 1
backend/db/migrations/20220403111037_create_tag_associations_table.js Переглянути файл

@@ -2,7 +2,7 @@ exports.up = function (knex) {
2 2
     return knex.schema.createTable('tag_associations', function (table) {
3 3
         table.increments('tag_association_id').primary()
4 4
         table.integer('profile_id').notNullable()
5
-        table.integer('membership_id')
5
+        table.integer('grouping_id')
6 6
         table.integer('tag_id').notNullable()
7 7
         table.boolean('is_deleted').notNullable()
8 8
     })

+ 6
- 6
backend/db/seeds/02-profiles.js Переглянути файл

@@ -1,21 +1,21 @@
1 1
 const mock = require('../data-generator/mock')
2 2
 const fs = require('fs')
3
-const dataSort = require ('../dataSort')
4
-const { batchSize } = require('../data-generator/config.json')
5
-
3
+const dataSort = require('../dataSort')
4
+const { batchSize, ignore } = require('../data-generator/config.json')
6 5
 
7 6
 let profiles = []
8 7
 const generatedDataPath = './db/generated'
9 8
 let fileNames = fs.readdirSync(generatedDataPath)
10 9
 for (let name of fileNames) {
11 10
     const data = require(`../generated/${name}`)
12
-    if(name[0] == '_') {
11
+    if (name[0] == '_') {
13 12
         profiles = [...profiles, ...data.profiles]
14 13
     }
15 14
 }
16
-// sort data
17
-profiles = dataSort(profiles, 'profile_id')
18 15
 
16
+profiles = dataSort(profiles, 'profile_id').filter(
17
+    profile => !ignore.includes(profile.profile_id),
18
+)
19 19
 
20 20
 exports.seed = async knex => {
21 21
     await knex('profiles').del()

+ 11
- 6
backend/db/seeds/04-responses.js Переглянути файл

@@ -1,20 +1,25 @@
1 1
 const mock = require('../data-generator/mock')
2 2
 const fs = require('fs')
3
-const dataSort = require ('../dataSort')
4
-const { batchSize } = require('../data-generator/config.json')
3
+const dataSort = require('../dataSort')
4
+const { batchSize, ignore } = require('../data-generator/config.json')
5 5
 
6 6
 let responses = []
7 7
 const generatedDataPath = './db/generated'
8 8
 let fileNames = fs.readdirSync(generatedDataPath)
9 9
 for (let name of fileNames) {
10 10
     const data = require(`../generated/${name}`)
11
-    if(name[0] == '_') {
11
+    if (name[0] == '_') {
12 12
         responses = [...responses, ...data.responses]
13 13
     }
14 14
 }
15 15
 
16
-// sort data
17
-responses = dataSort(responses, 'response_id')
16
+/**
17
+ * Prevent seeding responses for
18
+ * profile ids so we can test oboarding
19
+ */
20
+responses = dataSort(responses, 'response_id').filter(
21
+    response => !ignore.includes(response.profile_id),
22
+)
18 23
 
19 24
 exports.seed = async knex => {
20 25
     await knex('responses').del()
@@ -23,7 +28,7 @@ exports.seed = async knex => {
23 28
     for (let i = 1; i <= len; i += 1) {
24 29
         responsesToPush.push(responses.shift())
25 30
         if (i % batchSize === 0 || i > responses.length) {
26
-            await knex('responses').insert(responsesToPush)
31
+            // await knex('responses').insert(responsesToPush)
27 32
             responsesToPush = []
28 33
         }
29 34
     }

+ 5
- 0
backend/knexfile.js Переглянути файл

@@ -1,4 +1,5 @@
1 1
 require('dotenv').config()
2
+const fs = require('fs')
2 3
 
3 4
 const local = {
4 5
     host: process.env.DB_HOST,
@@ -8,6 +9,7 @@ const local = {
8 9
     port: process.env.DB_PORT,
9 10
 }
10 11
 const pscale = {
12
+    ssl: true,
11 13
     host: process.env.PSCALE_DB_HOST ? process.env.PSCALE_DB_HOST : '127.0.0.1',
12 14
     user: process.env.PSCALE_DB_USER ? process.env.PSCALE_DB_USER : 'root',
13 15
     password: process.env.PSCALE_DB_PASSWORD
@@ -33,5 +35,8 @@ module.exports = {
33 35
         seeds: {
34 36
             directory: './db/seeds',
35 37
         },
38
+        ssl: {
39
+            ca: fs.readFileSync('/etc/ssl/certs/ca-certificates.crt'),
40
+        },
36 41
     },
37 42
 }

+ 2
- 0
backend/lib/plugins/membership.js Переглянути файл

@@ -9,6 +9,7 @@ const MembershipService = require('../services/membership')
9 9
 const MembershipJoinRoute = require('../routes/membership/join')
10 10
 const MembershipLeaveRoute = require('../routes/membership/leave')
11 11
 const MembershipActiveRoute = require('../routes/membership/active')
12
+const MembershipRevealRoute = require('../routes/membership/reveal')
12 13
 
13 14
 module.exports = {
14 15
     name: 'membership-plugin',
@@ -29,5 +30,6 @@ module.exports = {
29 30
         await server.route(MembershipJoinRoute)
30 31
         await server.route(MembershipLeaveRoute)
31 32
         await server.route(MembershipActiveRoute)
33
+        await server.route(MembershipRevealRoute)
32 34
     },
33 35
 }

+ 4
- 0
backend/lib/plugins/profile.js Переглянути файл

@@ -21,6 +21,8 @@ const ProfileMatchRoute = require('../routes/profile/match')
21 21
 const ProfileQueueRoute = require('../routes/profile/queue')
22 22
 const ProfileGetRoute = require('../routes/profile/get')
23 23
 const ProfilePatchQueueRoute = require('../routes/profile/patch-queue')
24
+const TagRevealRoute = require('../routes/tag/reveal')
25
+const TagGetRoute = require('../routes/tag/get')
24 26
 
25 27
 module.exports = {
26 28
     name: 'profile-plugin',
@@ -53,5 +55,7 @@ module.exports = {
53 55
         await server.route(ProfileQueueRoute)
54 56
         await server.route(ProfileGetRoute)
55 57
         await server.route(ProfilePatchQueueRoute)
58
+        await server.route(TagRevealRoute)
59
+        await server.route(TagGetRoute)
56 60
     },
57 61
 }

+ 0
- 1
backend/lib/routes/membership/active.js Переглянути файл

@@ -74,7 +74,6 @@ module.exports = {
74 74
             let memberships = await membershipService.findMemberships(
75 75
                 groupings.map(grouping => grouping.grouping_id),
76 76
             )
77
-            // console.log('groupings :>> ', memberships)
78 77
 
79 78
             /**
80 79
              * Heavily process the result by storing just a profile_id

+ 104
- 0
backend/lib/routes/membership/reveal.js Переглянути файл

@@ -0,0 +1,104 @@
1
+const Joi = require('joi')
2
+
3
+const apiSchema = require('../../schemas/api')
4
+const errorSchema = require('../../schemas/errors')
5
+
6
+const pluginConfig = {
7
+    handlerType: 'reveal',
8
+    docs: {
9
+        description: 'reveal',
10
+        notes: 'Reveal profile information to a grouping by membership',
11
+    },
12
+}
13
+
14
+const validators = {
15
+    params: Joi.object({ grouping_id: Joi.number() }),
16
+    query: Joi.object({ profile_id: Joi.number(), tag_id: Joi.number() }),
17
+}
18
+
19
+const responseSchemas = {
20
+    response: Joi.object({
21
+        tags: Joi.array().items(),
22
+    }),
23
+    error: errorSchema.single,
24
+}
25
+module.exports = {
26
+    method: 'POST',
27
+    path: '/{grouping_id}/reveal',
28
+    options: {
29
+        ...pluginConfig.docs,
30
+        tags: ['api'],
31
+        auth: false,
32
+        cors: true,
33
+        handler: async function (request, h) {
34
+            const { membershipService, profileService } =
35
+                request.server.services()
36
+            const grouping_id = request.params.grouping_id
37
+            const { profile_id, tag_id } = request.query
38
+            try {
39
+                const tags = await profileService.revealProfileInfo({
40
+                    profile_id,
41
+                    grouping_id,
42
+                    tag_id,
43
+                    is_deleted: false,
44
+                })
45
+
46
+                // Notify both profiles that information has been revealed
47
+                const memberships = await membershipService.findMemberships([
48
+                    grouping_id,
49
+                ])
50
+                const idsInGroup = memberships.map(
51
+                    membership => membership.profile_id,
52
+                )
53
+                idsInGroup.forEach(profile_id => {
54
+                    request.server.methods.notify(
55
+                        `${profile_id}.stonk`,
56
+                        {
57
+                            name: 'REVEALED INFO',
58
+                            tag: tag_id,
59
+                            type: 'info',
60
+                        },
61
+                        h,
62
+                    )
63
+                })
64
+                return h
65
+                    .response({
66
+                        ok: true,
67
+                        handler: pluginConfig.handlerType,
68
+                        data: { tags },
69
+                    })
70
+                    .code(200)
71
+            } catch (err) {
72
+                return h
73
+                    .response({
74
+                        ok: false,
75
+                        handler: pluginConfig.handlerType,
76
+                        data: { error: `${err}` },
77
+                    })
78
+                    .code(409)
79
+            }
80
+        },
81
+
82
+        /** Validate based on validators object */
83
+        validate: {
84
+            ...validators,
85
+            failAction: 'log',
86
+        },
87
+
88
+        /** Validate the server response */
89
+        response: {
90
+            status: {
91
+                200: apiSchema.single
92
+                    .append({
93
+                        data: responseSchemas.response,
94
+                    })
95
+                    .label('reveal_res'),
96
+                409: apiSchema.single
97
+                    .append({
98
+                        data: responseSchemas.error,
99
+                    })
100
+                    .label('error_single_res'),
101
+            },
102
+        },
103
+    },
104
+}

+ 87
- 0
backend/lib/routes/tag/get.js Переглянути файл

@@ -0,0 +1,87 @@
1
+'use strict'
2
+
3
+const apiSchema = require('../../schemas/api')
4
+const errorSchema = require('../../schemas/errors')
5
+const params = require('../../schemas/params')
6
+const Joi = require('joi')
7
+
8
+const pluginConfig = {
9
+    handlerType: 'get',
10
+    docs: {
11
+        description: 'Get tags based on membership id',
12
+        notes: 'returns from the Tag Associations Table',
13
+    },
14
+}
15
+
16
+const responseSchemas = {
17
+    tags: Joi.array().items(Joi.object()),
18
+    error: errorSchema.single,
19
+}
20
+
21
+const validators = {
22
+    params: Joi.object({
23
+        profile_id: params.profileId,
24
+        grouping_id: params.groupingId,
25
+    }),
26
+    query: Joi.object({ category: Joi.string() }),
27
+}
28
+
29
+module.exports = {
30
+    method: 'GET',
31
+    path: '/{profile_id}/tags/{grouping_id}',
32
+    options: {
33
+        ...pluginConfig.docs,
34
+        tags: ['api'],
35
+        /** Protect this route with authentication? */
36
+        auth: false,
37
+        cors: true,
38
+        handler: async function (request, h) {
39
+            const { grouping_id, profile_id } = request.params
40
+            const { profileService } = request.server.services()
41
+            const { category } = request.query
42
+            const revealedTags = await profileService.getTagsFor(
43
+                profile_id,
44
+                grouping_id,
45
+                category,
46
+            )
47
+            try {
48
+                return h
49
+                    .response({
50
+                        ok: true,
51
+                        handler: pluginConfig.handlerType,
52
+                        data: revealedTags,
53
+                    })
54
+                    .code(200)
55
+            } catch (err) {
56
+                return h
57
+                    .response({
58
+                        ok: false,
59
+                        handler: pluginConfig.handlerType,
60
+                        data: { error: `${err}` },
61
+                    })
62
+                    .code(409)
63
+            }
64
+        },
65
+        /** Validate based on validators object */
66
+        validate: {
67
+            ...validators,
68
+            failAction: 'log',
69
+        },
70
+
71
+        /** Validate the server response */
72
+        response: {
73
+            status: {
74
+                200: apiSchema.single
75
+                    .append({
76
+                        data: responseSchemas.tags,
77
+                    })
78
+                    .label('tags_res'),
79
+                409: apiSchema.single
80
+                    .append({
81
+                        data: responseSchemas.error,
82
+                    })
83
+                    .label('error_single_res'),
84
+            },
85
+        },
86
+    },
87
+}

+ 91
- 0
backend/lib/routes/tag/reveal.js Переглянути файл

@@ -0,0 +1,91 @@
1
+'use strict'
2
+
3
+const apiSchema = require('../../schemas/api')
4
+const errorSchema = require('../../schemas/errors')
5
+const params = require('../../schemas/params')
6
+const Joi = require('joi')
7
+
8
+const pluginConfig = {
9
+    handlerType: 'reveal',
10
+    docs: {
11
+        description: 'Reveals part of a profile based on tag',
12
+        notes: 'returns from the Tag Associations Table',
13
+    },
14
+}
15
+
16
+const responseSchemas = {
17
+    tags: Joi.array().items(Joi.object()),
18
+    error: errorSchema.single,
19
+}
20
+
21
+const validators = {
22
+    params: params.profileId.append({ tag_id: Joi.number() }),
23
+    payload: Joi.object({
24
+        profile_id: Joi.number(),
25
+        membership_id: Joi.string().optional(),
26
+    }),
27
+}
28
+
29
+module.exports = {
30
+    method: 'POST',
31
+    path: '/{profile_id}/reveal/{tag_id}',
32
+    options: {
33
+        ...pluginConfig.docs,
34
+        tags: ['api'],
35
+        /** Protect this route with authentication? */
36
+        auth: false,
37
+        cors: true,
38
+        handler: async function (request, h) {
39
+            const { profile_id, tag_id } = request.params
40
+            const { profileService } = request.server.services()
41
+
42
+            const revealedTags = await profileService.revealProfileInfo({
43
+                profile_id,
44
+                tag_id,
45
+                is_deleted: false,
46
+            })
47
+            try {
48
+                const tag = profileService.tagLookup[tag_id]
49
+                if (!tag || tag.tag_category != 'reveal') {
50
+                    throw `cannot reveal ${tag} tag`
51
+                }
52
+                return h
53
+                    .response({
54
+                        ok: true,
55
+                        handler: pluginConfig.handlerType,
56
+                        data: revealedTags,
57
+                    })
58
+                    .code(200)
59
+            } catch (err) {
60
+                return h
61
+                    .response({
62
+                        ok: false,
63
+                        handler: pluginConfig.handlerType,
64
+                        data: { error: `${err}` },
65
+                    })
66
+                    .code(409)
67
+            }
68
+        },
69
+        /** Validate based on validators object */
70
+        validate: {
71
+            ...validators,
72
+            failAction: 'log',
73
+        },
74
+
75
+        /** Validate the server response */
76
+        response: {
77
+            status: {
78
+                200: apiSchema.single
79
+                    .append({
80
+                        data: responseSchemas.tags,
81
+                    })
82
+                    .label('reveal_res'),
83
+                409: apiSchema.single
84
+                    .append({
85
+                        data: responseSchemas.error,
86
+                    })
87
+                    .label('error_single_res'),
88
+            },
89
+        },
90
+    },
91
+}

+ 10
- 3
backend/lib/schemas/params.js Переглянути файл

@@ -1,12 +1,19 @@
1 1
 const Joi = require('joi')
2 2
 
3 3
 module.exports = {
4
-    profileId: Joi.object({ profile_id: Joi.number() }).label('profile_id_param'),
4
+    profileId: Joi.object({ profile_id: Joi.number() }).label(
5
+        'profile_id_param',
6
+    ),
7
+    groupingId: Joi.object({ grouping_id: Joi.number() }).label(
8
+        'grouping_id_param',
9
+    ),
5 10
     profileInclude: Joi.object({ include_profile: Joi.bool() }),
6 11
     userId: Joi.object({ user_id: Joi.number() }).label('user_id_param'),
7 12
     userName: Joi.object({
8 13
         name: Joi.string().min(3).max(11),
9 14
         all: Joi.array(),
10 15
     }).label('user_name_param'),
11
-    userEmail: Joi.object({ user_email: Joi.string() }).label('user_email_param')
12
-}
16
+    userEmail: Joi.object({ user_email: Joi.string() }).label(
17
+        'user_email_param',
18
+    ),
19
+}

+ 1
- 1
backend/lib/schemas/tags.js Переглянути файл

@@ -12,7 +12,7 @@ const singleTag = Joi.object({
12 12
 const singleTagAssociation = Joi.object({
13 13
     tag_association_id: Joi.number(),
14 14
     profile_id: Joi.number().required(),
15
-    membership_id: Joi.number(),
15
+    grouping_id: Joi.number(),
16 16
     tag_id: Joi.number().required(),
17 17
     is_deleted: Joi.boolean().required(),
18 18
 }).label('tag_association_single')

+ 33
- 1
backend/lib/services/profile/index.js Переглянути файл

@@ -11,7 +11,7 @@ module.exports = class ProfileService extends Schmervice.Service {
11 11
         super(...args)
12 12
         /** Scores available in the db to map against score indices*/
13 13
         this.scoreLookup = {}
14
-        /** Tags available in the db to map against tagg_associations*/
14
+        /** Tags available in the db to map against tag_associations*/
15 15
         this.tagLookup = {}
16 16
         // this.responseKeyLookup = ResponseKey.query()
17 17
     }
@@ -345,4 +345,36 @@ module.exports = class ProfileService extends Schmervice.Service {
345 345
         const end = await this._latLonForZip(end_zip)
346 346
         return haversine(start, end, { unit: distanceUnit })
347 347
     }
348
+
349
+    /**
350
+     * Use the db to grab tag associations
351
+     * by profile and match them to tag types
352
+     * @param {number} profileId
353
+     * @param {object}
354
+     */
355
+    async getTagsFor(profileId, groupingId, category) {
356
+        const { TagAssociation } = this.server.models()
357
+        await this._setTagLookup()
358
+        let associations = groupingId
359
+            ? await TagAssociation.query()
360
+                  .where('grouping_id', groupingId)
361
+                  .andWhere('profile_id', profileId)
362
+            : await TagAssociation.query().andWhere('profile_id', profileId)
363
+        return associations
364
+            .map(assoc => ({
365
+                ...assoc,
366
+                tag: this.tagLookup[assoc.tag_id],
367
+            }))
368
+            .filter(tagWithAssoc => {
369
+                return category
370
+                    ? tagWithAssoc.tag.tag_category == category
371
+                    : true
372
+            })
373
+    }
374
+    async revealProfileInfo(association) {
375
+        const { TagAssociation } = this.server.models()
376
+        await TagAssociation.query().insert(association)
377
+
378
+        return await this.getTagsFor(association.profile_id)
379
+    }
348 380
 }

+ 1
- 1
backend/lib/services/profile/profiler.js Переглянути файл

@@ -14,7 +14,7 @@ class CompleteProfile {
14 14
         this.user_email = profile.user.user_email
15 15
         this.responses = []
16 16
         this.user_type = type
17
-        this.tags = profile.tags.filter(t => t.category != 'reveal')
17
+        this.tags = profile.tags.filter(t => t.tag_category != 'reveal')
18 18
 
19 19
         // TODO: generalize this for multiple images, and languages
20 20
         this.profile_description = ''

+ 130
- 8357
backend/package-lock.json
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 2
- 1
backend/package.json Переглянути файл

@@ -25,10 +25,11 @@
25 25
         "@hapipal/confidence": "^6.0.1",
26 26
         "@hapipal/schmervice": "^2.0.0",
27 27
         "@hapipal/schwifty": "^6.0.0",
28
+        "@planetscale/database": "^1.4.0",
28 29
         "compute-cosine-similarity": "^1.0.0",
29 30
         "dotenv": "^10.0.0",
30 31
         "exiting": "^6.0.1",
31
-        "hapi-swagger": "^14.2.5",
32
+        "hapi-swagger": "^14.5.5",
32 33
         "haversine": "^1.1.1",
33 34
         "joi": "^17.4.0",
34 35
         "knex": "^0.21.19",

+ 1
- 0
backend/server/index.js Переглянути файл

@@ -13,6 +13,7 @@ exports.deployment = async ({ start } = {}) => {
13 13
     if (start) {
14 14
         await Exiting.createManager(server).start()
15 15
         server.log(['start'], `Server started at ${server.info.uri}`)
16
+        process.title = 'siimee_backend'
16 17
         return server
17 18
     }
18 19
 

+ 41
- 7
backend/server/manifest.js Переглянути файл

@@ -1,10 +1,43 @@
1
-const Dotenv = require('dotenv').config({ path: './server/.env' })
1
+require('dotenv').config()
2 2
 const Confidence = require('@hapipal/confidence')
3 3
 const Inert = require('@hapi/inert')
4 4
 const Vision = require('@hapi/vision')
5 5
 const Schwifty = require('@hapipal/schwifty')
6 6
 const HapiSwagger = require('hapi-swagger')
7 7
 
8
+const confs = {
9
+    local: {
10
+        db: process.env.DB_NAME,
11
+        host: process.env.DB_HOST,
12
+        port: process.env.DB_PORT,
13
+        ssl: false,
14
+        user: process.env.DB_USER,
15
+        pw: process.env.DB_ROOT_PASSWORD,
16
+    },
17
+    prod: {
18
+        db: process.env.PSCALE_DB_NAME,
19
+        host: process.env.PSCALE_DB_HOST,
20
+        port: process.env.PSCALE_DB_PORT,
21
+        branch: process.env.PSCALE_DB_BRANCH,
22
+        ssl: true,
23
+        user: process.env.PSCALE_DB_USER,
24
+        pw: process.env.PSCALE_DB_PASSWORD,
25
+    },
26
+    dbFlavor: process.env.DB_TYPE,
27
+    useLocalDb: () => {
28
+        return process.env.USE_LOCAL_DB == 'true'
29
+    },
30
+}
31
+
32
+const _current = {
33
+    ssl: confs.useLocalDb() ? confs.local.ssl : confs.prod.ssl,
34
+    host: confs.useLocalDb() ? confs.local.host : confs.prod.host,
35
+    port: confs.useLocalDb() ? confs.local.port : confs.prod.port,
36
+    database: confs.useLocalDb() ? confs.local.db : confs.prod.db,
37
+    user: confs.useLocalDb() ? confs.local.user : confs.prod.user,
38
+    password: confs.useLocalDb() ? confs.local.pw : confs.prod.pw,
39
+}
40
+
8 41
 /** Glue manifest as a confidence store */
9 42
 module.exports = new Confidence.Store({
10 43
     server: {
@@ -70,14 +103,15 @@ module.exports = new Confidence.Store({
70 103
                     $base: {
71 104
                         migrateOnStart: true,
72 105
                         knex: {
73
-                            client: process.env.DB_TYPE,
106
+                            client: confs.dbFlavor,
74 107
                             useNullAsDefault: true,
75 108
                             connection: {
76
-                                host: process.env.DB_HOST,
77
-                                user: process.env.DB_USER,
78
-                                password: process.env.DB_ROOT_PASSWORD,
79
-                                database: process.env.DB_NAME,
80
-                                port: process.env.DB_PORT,
109
+                                database: _current.database,
110
+                                host: _current.host,
111
+                                port: _current.port,
112
+                                ssl: _current.ssl,
113
+                                user: _current.user,
114
+                                password: _current.password,
81 115
                             },
82 116
                         },
83 117
                     },

+ 5
- 5
deployment/post-receive Переглянути файл

@@ -1,5 +1,4 @@
1 1
 #!/bin/bash
2
-
3 2
 REPO="/opt/staging/siimee.git"
4 3
 commands=(
5 4
     "cd ${REPO}"
@@ -8,9 +7,9 @@ commands=(
8 7
     "echo no build needed!"
9 8
     "cd ${REPO}/frontend && rm -f ${REPO}/frontend/package-lock.json && npm install"
10 9
     "npm run build"
11
-    "cd ${REPO}/backend && npm run generate && npm run reseed"
10
+    # "cd ${REPO}/backend && npm run generate && npm run reseed"
11
+    "pm2 restart siimee_backend -i -1 -n siimee_backend"
12 12
 )
13
-
14 13
 steps=(
15 14
     "navigate to project…"
16 15
     "git checkout…"
@@ -18,9 +17,10 @@ steps=(
18 17
     "building backend…"
19 18
     "frontend npm install…"
20 19
     "building frontend…"
21
-    "regenerate db data…"
20
+    # "regenerate db data…"
21
+    "start-up…"
22 22
 )
23
-SPAN=92
23
+SPAN=80
24 24
 COUNT=0
25 25
 hr() {
26 26
     printf "\n"

+ 1
- 0
frontend/assets/icons/calendar.svg Переглянути файл

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path d="M19.378,3.822H18.267V6.044H14.933V3.822H8.267V6.044H4.933V3.822H3.822A2.228,2.228,0,0,0,1.6,6.044V19.378A2.228,2.228,0,0,0,3.822,21.6H19.378A2.229,2.229,0,0,0,21.6,19.378V6.044a2.229,2.229,0,0,0-2.222-2.222Zm0,15.556H3.822V10.489H19.378ZM7.711,1.6H5.489V5.489H7.711Zm10,0H15.489V5.489h2.222Z" transform="translate(-1.6 -1.6)" fill="#fff"/></svg>

+ 1
- 0
frontend/assets/icons/chat.svg Переглянути файл

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="27.778" height="25" viewBox="0 0 27.778 25"><path d="M8.056,17.156V8.544H2.778A2.786,2.786,0,0,0,0,11.322v8.333a2.786,2.786,0,0,0,2.778,2.778H4.167V26.6l4.167-4.167h6.944a2.786,2.786,0,0,0,2.778-2.778V17.128a1.328,1.328,0,0,1-.278.03l-9.722,0ZM25,1.6H12.5A2.786,2.786,0,0,0,9.722,4.378V15.489h9.722l4.167,4.167V15.489H25a2.785,2.785,0,0,0,2.778-2.778V4.378A2.786,2.786,0,0,0,25,1.6Z" transform="translate(0 -1.6)" fill="#fff"/></svg>

+ 1
- 0
frontend/assets/icons/close-icon.svg Переглянути файл

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="17.414" height="17.414" viewBox="0 0 17.414 17.414"><g transform="translate(0.707 0.707)"><line x1="16" y1="16" fill="none" stroke="#fff" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1"/><line y1="16" x2="16" fill="none" stroke="#fff" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1"/></g></svg>

+ 1
- 0
frontend/assets/icons/options-icon.svg Переглянути файл

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="19" height="5" viewBox="0 0 19 5"><g fill="none" stroke="#fff" stroke-width="1"><circle cx="2.5" cy="2.5" r="2.5" stroke="none"/><circle cx="2.5" cy="2.5" r="2" fill="none"/></g><g transform="translate(7)" fill="none" stroke="#fff" stroke-width="1"><circle cx="2.5" cy="2.5" r="2.5" stroke="none"/><circle cx="2.5" cy="2.5" r="2" fill="none"/></g><g transform="translate(14)" fill="none" stroke="#fff" stroke-width="1"><circle cx="2.5" cy="2.5" r="2.5" stroke="none"/><circle cx="2.5" cy="2.5" r="2" fill="none"/></g></svg>

BIN
frontend/assets/logos/siimee_logo.jpg Переглянути файл


+ 11
- 0
frontend/package-lock.json Переглянути файл

@@ -8,6 +8,7 @@
8 8
             "name": "vite-project",
9 9
             "version": "0.0.0",
10 10
             "dependencies": {
11
+                "chart.js": "^3.9.1",
11 12
                 "joi": "^17.6.0",
12 13
                 "pubnub": "^5.0.0",
13 14
                 "vue": "^3.2.31",
@@ -1288,6 +1289,11 @@
1288 1289
                 "is-regex": "^1.0.3"
1289 1290
             }
1290 1291
         },
1292
+        "node_modules/chart.js": {
1293
+            "version": "3.9.1",
1294
+            "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
1295
+            "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="
1296
+        },
1291 1297
         "node_modules/chokidar": {
1292 1298
             "version": "3.5.3",
1293 1299
             "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -6918,6 +6924,11 @@
6918 6924
                 "is-regex": "^1.0.3"
6919 6925
             }
6920 6926
         },
6927
+        "chart.js": {
6928
+            "version": "3.9.1",
6929
+            "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
6930
+            "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="
6931
+        },
6921 6932
         "chokidar": {
6922 6933
             "version": "3.5.3",
6923 6934
             "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",

+ 1
- 0
frontend/package.json Переглянути файл

@@ -12,6 +12,7 @@
12 12
         "test": "ava"
13 13
     },
14 14
     "dependencies": {
15
+        "chart.js": "^3.9.1",
15 16
         "joi": "^17.6.0",
16 17
         "pubnub": "^5.0.0",
17 18
         "vue": "^3.2.31",

+ 12
- 12
frontend/src/App.vue Переглянути файл

@@ -1,16 +1,11 @@
1 1
 <template lang="pug">
2 2
 w-app 
3
-    TopNav(@on-open="openDrawer = !openDrawer")
3
+    TopNav(@on-open='openDrawer = !openDrawer')
4 4
 
5
-    w-drawer(v-model="openDrawer")
6
-        SideBar(@updatePid="setPid" :pid="profile.id.value")
7
-        
8
-    RouterView(
9
-        v-if="profile.isLoggedIn"
10
-        :pid="profile.id.value"
11
-        @updatePid="setPid"
12
-        @show-sidebar="showSidebar = !showSidebar"
13
-    )
5
+    w-drawer(v-model='openDrawer')
6
+        SideBar(:pid='profile.id.value' @updatePid='setPid')
7
+
8
+    RouterView(@updatePid='setPid')
14 9
 </template>
15 10
 
16 11
 <script>
@@ -22,8 +17,7 @@ import { currentProfile } from './services'
22 17
 import { surveyFactory } from './utils'
23 18
 
24 19
 const DEV_MODE = import.meta.env.VITE_DEV == 'true'
25
-// const DEV_MODE = false
26
-const DEV_PID = 45
20
+const DEV_PID = 46
27 21
 
28 22
 export default {
29 23
     components: { TopNav, SideBar },
@@ -46,6 +40,12 @@ export default {
46 40
          * using the login form
47 41
          */
48 42
         if (DEV_MODE) {
43
+            console.info('===============================================')
44
+            console.info('-                   SIIMEE                    -')
45
+            console.info('-----------------------------------------------')
46
+            console.info('[Siimee App]: You are in development mode:', DEV_MODE)
47
+            console.info('[Siimee App]: Starting application...')
48
+            console.info('-----------------------------------------------')
49 49
             await this.setPid(DEV_PID)
50 50
         }
51 51
     },

+ 19
- 2
frontend/src/components/AspectBar.vue Переглянути файл

@@ -1,8 +1,8 @@
1 1
 <template lang="pug">
2 2
 figure.w-flex.column
3 3
     figcaption.w-flex.xs12.justify-space-between.align-center
4
-        p(v-for="label in labels" :key="label").text-upper {{ label }}
5
-    w-progress(v-model="percentage" size="0.5em" round).mt4
4
+        p(v-for="(label, index) in labels" :key="label" :class="{ 'main': index === 1 }")  {{ label }}
5
+    w-progress(v-model="percentage" size="0.5em" round).mb7
6 6
 </template>
7 7
 
8 8
 <script>
@@ -21,3 +21,20 @@ export default {
21 21
     data: () => ({}),
22 22
 }
23 23
 </script>
24
+<style lang="sass">
25
+    figure
26
+        figcaption
27
+            font-family: Century Gothic, CenturyGothic, AppleGothic, sans-serif
28
+            text-transform: capitalize
29
+            .main
30
+                color: #4D9127
31
+                font-weight: bold
32
+                text-transform: uppercase
33
+
34
+
35
+        .w-progress
36
+            background-color: #4C5264
37
+            .w-progress__progress
38
+                color: #4D9127
39
+        
40
+</style>

+ 29
- 0
frontend/src/components/ChatBubble.vue Переглянути файл

@@ -0,0 +1,29 @@
1
+<template lang="pug">
2
+.chat-blob.message-wrapper.pa4
3
+    p.pb1
4
+        span(v-if='publisherId == profile.id.value') you
5
+        span(v-else) {{ publisherId }}
6
+        span @{{ new Date(parseInt(time) / 10000).toLocaleTimeString('en-US') }}
7
+    p {{ message }}
8
+</template>
9
+
10
+<script>
11
+import { mixins } from '../utils'
12
+export default {
13
+    props: {
14
+        publisherId: {
15
+            type: String,
16
+            required: true,
17
+        },
18
+        message: {
19
+            type: String,
20
+            required: true,
21
+        },
22
+        time: {
23
+            type: String,
24
+            required: true,
25
+        },
26
+    },
27
+    mixins: [mixins.profileMixin],
28
+}
29
+</script>

+ 41
- 0
frontend/src/components/DynamicTagList.vue Переглянути файл

@@ -0,0 +1,41 @@
1
+<template lang="pug">
2
+.ListControls(style="display:flex; flex-direction:row;")
3
+    input(type="text" :placeholder='`Add ${placeholder}...`' v-model="newTag")
4
+    w-button.ma1(@click="addTag") +
5
+TagList(:tags="mutableTags")
6
+</template>
7
+<script>
8
+import TagList from './TagList.vue'
9
+export default {
10
+    name: 'DynamicTagList',
11
+    components:{
12
+        TagList,
13
+    },
14
+    props:{
15
+        tags: {
16
+            required: true,
17
+            type: Array,
18
+        },
19
+        placeholder:{
20
+            required: true,
21
+            type: String,
22
+        }
23
+    },
24
+    data() {
25
+        return {
26
+            newTag: null,
27
+            mutableTags: this.tags,
28
+        }    
29
+    },
30
+    methods:{
31
+        addTag(){
32
+            if(!this.newTag){
33
+                console.warn('Type a new tag before adding')
34
+                return
35
+            }
36
+            this.mutableTags.push(this.newTag)
37
+            this.newTag = ''
38
+        }
39
+    }
40
+}
41
+</script>

+ 1
- 1
frontend/src/components/Messages.vue Переглянути файл

@@ -2,7 +2,7 @@
2 2
 .sidebar--messages
3 3
   h5.message__title matches
4 4
   router-link(
5
-        :to="`/chats/${match.profile.profile_id}`" 
5
+        :to="`/chat/${match.profile.profile_id}`" 
6 6
         v-for='match in matches' 
7 7
         :key='match.profile.profile_id' 
8 8
         :class="[pid == match.profile.profile_id ? 'active' : '', 'sidebar__message', 'f-col', 'start']"

+ 36
- 10
frontend/src/components/NamePlate.vue Переглянути файл

@@ -1,14 +1,13 @@
1 1
 <template lang="pug">
2 2
 .name-plate.xs12.w-flex.justify-center
3
-    section(v-if="pid" :class="{ box: !isList }")
4
-        router-link(:to="`/profile/${pid}`" disabled)
3
+    section(:class='{ box: !isList }' v-if='pid')
4
+        router-link(:to='`/profile/${pid}`' disabled)
5 5
             h1.text-capitalize {{ name }}
6 6
                 span O
7
-            p.text-capitalize {{role}}&nbsp;
8
-                span.text-capitalize(v-if="isList") |&nbsp;
9
-                    span.text-capitalize location, st
10
-            p.text-capitalize(v-if="!isList") {{ pronouns }}&nbsp;
11
-                span.text-capitalize | ethnicity
7
+            p.text-capitalize {{ role }}&nbsp;
8
+                span.text-capitalize(v-if='isList')
9
+                    span.text-capitalize | {{ locale }}
10
+            p.text-capitalize(v-if='!isList') {{ pronouns }}
12 11
 </template>
13 12
 
14 13
 <script>
@@ -22,6 +21,14 @@ export default {
22 21
             type: String,
23 22
             required: true,
24 23
         },
24
+        role: {
25
+            type: String,
26
+            required: true,
27
+        },
28
+        locale: {
29
+            type: String,
30
+            required: true,
31
+        },
25 32
         pronouns: {
26 33
             type: String,
27 34
             required: true,
@@ -32,7 +39,6 @@ export default {
32 39
             default: true,
33 40
         },
34 41
     },
35
-    data: () => ({}),
36 42
 }
37 43
 </script>
38 44
 <style lang="sass">
@@ -42,12 +48,32 @@ export default {
42 48
         flex-direction: column
43 49
         align-items: center
44 50
         justify-content: center
45
-        padding: 15px 15px
51
+        padding: 15px
46 52
         min-height: 10vh
47 53
         width: 100%
48 54
         &.box
49
-            border: 1px black solid
55
+            background-color: #D5D5D5
56
+            border-radius: 6px
50 57
             height: 15vw
51 58
             width: 15vw
52 59
             text-align: center
60
+            font-family: Century Gothic, CenturyGothic, AppleGothic, sans-serif
61
+            h1
62
+                font-weight: bold
63
+                font-size: 1.619em
64
+                color: #183770
65
+            p
66
+                font-weight: bolder
67
+                font-size: 0.8095em
68
+                color: #183770
69
+        h1
70
+            font-weight: bold
71
+            font-size: 1.619em
72
+            color: #F7F5A6
73
+            text-align: center
74
+        p
75
+            font-weight: bolder
76
+            font-size: 1em
77
+            color: #F7F5A6
78
+            text-align: center
53 79
 </style>

+ 44
- 6
frontend/src/components/PairingButton.vue Переглянути файл

@@ -1,16 +1,28 @@
1 1
 <template lang="pug">
2 2
 .pairing-button.w-flex.row
3
-    w-button.xs6.mt4.mb4(@click='pass' bg-color='red' color='white' xl)
4
-        p.pa4.text-upper pass
5
-    w-button.xs6.mt4.mb4(@click='pair' bg-color='green' color='mint-green' xl)
6
-        p.pa4.text-upper pair
3
+    template(v-if='status == "pristine"')
4
+        w-button(:class='status' @click='pass')
5
+            p.pa4.text-upper pass
6
+        w-button(:class='status' @click='pair')
7
+            p.pa4.text-upper pair
8
+    template(v-else-if='status == "pending"')
9
+        w-button(:class='status')
10
+            p.pa4.text-upper pending
11
+    template(v-else)
12
+        w-button(:class='status')
13
+            p.pa4.text-upper paired
7 14
 </template>
8 15
 
9 16
 <script>
10 17
 export default {
11
-    props: {},
18
+    props: {
19
+        status: {
20
+            required: false,
21
+            type: String,
22
+            default: 'pristine',
23
+        },
24
+    },
12 25
     emits: ['pair', 'pass'],
13
-    data: () => ({}),
14 26
     methods: {
15 27
         pair() {
16 28
             this.$emit('pair')
@@ -21,3 +33,29 @@ export default {
21 33
     },
22 34
 }
23 35
 </script>
36
+
37
+<style lang="sass">
38
+.w-button
39
+    p
40
+        font-size: 1.6em
41
+        font-weight: bold
42
+    &.pristine
43
+        background-color: #000
44
+        border: 2px solid #4D9127
45
+        max-width: 350px
46
+        width: 50%
47
+        margin: 11px 0
48
+        padding: 22px
49
+    &.pending
50
+        background-image: linear-gradient(to right, #4C5264, #A8A8A8)
51
+        min-width: 350px
52
+        width: 100%
53
+        margin: 11px 0
54
+        padding: 22px
55
+    &.paired
56
+        background: #8168F8
57
+        min-width: 350px
58
+        width: 100%
59
+        margin: 11px 0
60
+        padding: 22px
61
+</style>

+ 39
- 20
frontend/src/components/PairsList.vue Переглянути файл

@@ -2,26 +2,30 @@
2 2
 section.pairs-list
3 3
     article(v-if='pairs.length')
4 4
         template(v-for='pair in pairs')
5
-            router-link.pair.w-flex.align-center.justify-space-around(
6
-                :to='`/profile/${pair.profile.pid}`'
7
-                v-if='tabName == "pending"'
8
-            )
9
-            router-link.pair.w-flex.align-center.justify-space-around(
10
-                :to='`/chat/${pair.profile.pid}`'
11
-                v-else
12
-            )
13
-                .dot--icon
14
-                .avatar
15
-                .idCard
16
-                    p {{ pair.profile.name }}
17
-                    p {{ pair.profile.pid }}
18
-                    p since: {{ pair.grouping.createdAt }}
19
-                    p updated: {{ pair.grouping.lastUpdatedAt }}
20
-                    p paired: {{ pair.grouping.is_paired }}
5
+            w-flex().align-center.flex-start
6
+                router-link.pair.w-flex.align-center.flex-start(
7
+    :to='`/profile/${pair.profile.pid}`')
8
+                    .dot--icon
9
+                    .avatar
10
+                    .idCard
11
+                        h3 {{ pair.profile.name }} {{ pair.profile.pid }}
12
+                        p registered nurse
13
+
14
+                w-menu( left v-model='showMenu')
15
+                    template(#activator)
16
+                        w-button.mr3(@click='showMenu = !showMenu' icon="mdi mdi-dots-horizontal")
17
+                    w-flex()
18
+                        router-link(
19
+                            :to='`/chat/${pair.profile.pid}`')
20
+                            w-button.mx2(@click='showMenu = false' bg-color="success" tile icon="mdi mdi-forum") Chat
21
+                        w-button.mx2(@click='showMenu = false' bg-color="info" tile icon="mdi mdi-calendar") Calendar
22
+                        w-button.mx2(@click='showMenu = false' bg-color="primary" icon="wi-cross")
23
+
21 24
     p(v-else) No {{ tabName }} profiles.
22 25
 </template>
23 26
 
24 27
 <script setup>
28
+import { ref } from 'vue'
25 29
 const props = defineProps({
26 30
     pairs: {
27 31
         type: [Object, Array],
@@ -32,18 +36,33 @@ const props = defineProps({
32 36
         default: 'paired',
33 37
     },
34 38
 })
39
+const showMenu = ref(false)
35 40
 </script>
36 41
 
37 42
 <style lang="sass">
38 43
 .pairs-list
44
+    color: #fff
39 45
     article
46
+        font-family: 'Century Gothic'
40 47
         .dot--icon
41
-            width:3vw
42
-            height:3vw
48
+            width:12px
49
+            height:12px
50
+            margin: 11px
43 51
             border-radius:50%
44 52
             background-color:#60C3FF
45 53
         .avatar
46
-            width:10vw
47
-            height:10vw
54
+            width:40px
55
+            height:40px
56
+            margin: 11px
57
+            border-radius: 6px
48 58
             background-color:#D5D5D5
59
+        .idCard
60
+            color: #fff
61
+            margin: 11px
62
+            h3
63
+                font-size: 16px
64
+            p
65
+                font-size: 14px
66
+.w-menu--card
67
+    background-color: #000000 !important
49 68
 </style>

+ 56
- 23
frontend/src/components/ProfileCard.vue Переглянути файл

@@ -1,21 +1,28 @@
1 1
 <template lang="pug">
2
-w-card.profile-card-list--card.xs12.pa12
2
+w-card.profile-card-list--card.xs12
3 3
     header.xs12.w-flex.column.center
4 4
         NamePlate(
5
+            :ethnicity='card.ethnicity'
5 6
             :is-list='isList'
6 7
             :is-paired='isPaired'
8
+            :locale='card.locale'
7 9
             :name='card.name'
8 10
             :pid='card.pid'
9 11
             :pronouns='card.pronouns'
10 12
             :role='card.role'
11 13
         )
12 14
 
13
-        w-button.text-upper.xs12.pa6(v-if='isPaired && !isList')
14
-            w-icon.mr1(xl) mdi mdi-chat
15
-            | start chat
16
-
17 15
         template(v-if='!isList')
18
-            SummaryBar(:is-tab='isPaired' :tab-content='card.summary')
16
+            w-button.text-upper.xs12.pa6(v-if='currentTab == 0 && isPaired')
17
+                w-icon.mr1(xl) mdi mdi-chat
18
+                | start chat
19
+
20
+            SummaryBar(
21
+                :aspects='aspects'
22
+                :is-tab='isPaired'
23
+                :tab-content='card.summary'
24
+                @tab-change='onTab'
25
+            )
19 26
             TagList(v-if='!isPaired || isList')
20 27
 
21 28
     article.xs12.w-flex.column.justify-space-between
@@ -28,19 +35,18 @@ w-card.profile-card-list--card.xs12.pa12
28 35
         )
29 36
 
30 37
     footer(v-if='!isList && !isPaired')
31
-        .pa12
38
+        .px3
32 39
             p {{ card.summary.about.tab }}
33 40
         PairingButton(@pair='onPair' @pass='onPass' v-if='!isPaired')
41
+        w-button.text-upper.xs12.pa6(v-else-if='currentTab != 0')
42
+            w-icon.mr1(xl) mdi mdi-chat
43
+            | start chat
34 44
 </template>
35 45
 
36 46
 <script setup>
37 47
 import { ref } from 'vue'
38 48
 import { useRouter } from 'vue-router'
39
-import {
40
-    updateQueueByProfileId,
41
-    postMembershipByProfileId,
42
-    currentProfile,
43
-} from '../services'
49
+import { postMembershipByProfileId, currentProfile } from '../services'
44 50
 
45 51
 import NamePlate from './NamePlate.vue'
46 52
 import AspectBar from './AspectBar.vue'
@@ -68,24 +74,33 @@ const props = defineProps({
68 74
     },
69 75
 })
70 76
 
77
+/**
78
+ * Track tab state for conditional rendering
79
+ */
80
+const currentTab = ref(0)
81
+const onTab = tabIndex => {
82
+    if (currentTab.value == tabIndex) return
83
+    currentTab.value = tabIndex
84
+}
85
+
71 86
 /**
72 87
  * Attempt to pair with target profile
73 88
  * Creates a grouping, and a membership
74 89
  * for both profileId and targetId
75 90
  */
76 91
 const onPair = async () => {
77
-    const group = await postMembershipByProfileId({
78
-        profileId: currentProfile.id.value,
79
-        targetId: props.card.pid,
92
+    currentProfile._loading = true
93
+    const profileId = currentProfile.id.value
94
+    const targetId = props.card.pid
95
+    await postMembershipByProfileId({
96
+        profileId,
97
+        targetId,
80 98
     })
81
-    updateQueueByProfileId(currentProfile.id.value, props.card.pid, false)
82
-    currentProfile.getGroupings()
83
-    console.warn('created grouping:', group)
99
+    await currentProfile.updateQueue(profileId, targetId, false)
100
+    await currentProfile.getGroupings()
101
+    currentProfile._loading = false
84 102
 
85 103
     let goToRoute = { name: 'HomeView' }
86
-    // if (group.membershipMatch.hasMatch) {
87
-    //     goToRoute = { name: 'PairsView' }
88
-    // }
89 104
     router.push(goToRoute)
90 105
 }
91 106
 
@@ -94,8 +109,16 @@ const onPair = async () => {
94 109
  * and forward back home
95 110
  */
96 111
 const onPass = async () => {
97
-    updateQueueByProfileId(currentProfile.id.value, props.card.pid, true)
98
-    router.push({ name: 'HomeView' })
112
+    currentProfile._loading = true
113
+    await currentProfile.updateQueue(
114
+        currentProfile.id.value,
115
+        props.card.pid,
116
+        true,
117
+    )
118
+    currentProfile._loading = false
119
+
120
+    let goToRoute = { name: 'HomeView' }
121
+    router.push(goToRoute)
99 122
 }
100 123
 </script>
101 124
 
@@ -103,7 +126,17 @@ const onPass = async () => {
103 126
 .profile-card-list--card
104 127
     background-color: #000
105 128
     color: #fff
129
+    width: 100%
130
+    max-width: 450px
131
+    margin: 11px auto
106 132
     header > .w-button
107 133
         background-color: #116006
108 134
         color: #fff
135
+    footer
136
+        margin-bottom: 22px
137
+        p
138
+            font-family: Century Gothic, CenturyGothic, AppleGothic, sans-serif
139
+            margin: 11px auto
140
+            padding: 0 7px
141
+            line-height: 1.619em
109 142
 </style>

+ 9
- 85
frontend/src/components/ProfileCardList.vue Переглянути файл

@@ -1,46 +1,21 @@
1 1
 <template lang="pug">
2 2
 section.profile-card-list.xs12.w-flex.column
3
-    header.xs12.w-flex 
4
-        w-select(:items="['one', 'two', 'three']" outline) Label
5
-    
6 3
     article
7 4
         ProfileCard.match-layout(
8
-            v-for="(card, i) in cards"
9
-            :key="`${card.pid}-${i}`"
10
-            :card="card"
11
-            :aspects="aspects"
12
-            :is-list="true"
5
+            :aspects='aspects'
6
+            :card='card'
7
+            :is-list='true'
8
+            :key='`${card.pid}-${i}`'
9
+            v-for='(card, i) in cards'
13 10
         )
14 11
 </template>
15 12
 
16 13
 <script setup>
17 14
 import { ref } from 'vue'
18
-import { useRouter } from 'vue-router'
19
-import {
20
-    updateQueueByProfileId,
21
-    postMembershipByProfileId,
22
-    currentProfile,
23
-} from '../services'
15
+import { cardAspects } from '../entities'
24 16
 import ProfileCard from './ProfileCard.vue'
25 17
 
26
-class Aspect {
27
-    constructor({ name, labels, percentage = 50 }) {
28
-        this.name = name
29
-        this.labels = labels
30
-        this.percentage = percentage
31
-    }
32
-}
33
-const aspects = ref([
34
-    new Aspect({ name: 'creativity', labels: ['creative', 'methodical'] }),
35
-    new Aspect({ name: 'dynamism', labels: ['dynamic', 'ordered'] }),
36
-    new Aspect({ name: 'precision', labels: ['precise', 'resourceful'] }),
37
-    new Aspect({ name: 'vision', labels: ['visionary', 'implementer'] }),
38
-    new Aspect({ name: 'focus', labels: ['big picture', 'focused'] }),
39
-    new Aspect({ name: 'attention', labels: ['guided', 'self-managed'] }),
40
-])
41
-
42
-const router = useRouter()
43
-const emit = defineEmits(['reload'])
18
+const aspects = ref(cardAspects)
44 19
 
45 20
 const props = defineProps({
46 21
     cards: {
@@ -51,63 +26,12 @@ const props = defineProps({
51 26
                 name: 'Full Name',
52 27
                 avatar: 'https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/newborn-baby-boy-sleeping-peacefully-wearing-knit-royalty-free-image-1589459736.jpg?crop=0.669xw:1.00xh;0.228xw,0&resize=640:*',
53 28
                 metadata: { age: '21', rawMetadata: 'Some Text Here!' },
29
+                role: 'more filler',
30
+                ethnicity: 'some background',
54 31
             },
55 32
         ],
56 33
     },
57
-    pid: {
58
-        type: Number,
59
-        default: 9999,
60
-    },
61
-    isGrid: {
62
-        type: Boolean,
63
-    },
64 34
 })
65
-
66
-// AHP Button behavior
67
-const accept = async targetId => {
68
-    if (targetId == props.pid) return
69
-    // need to pass these arguments (profileId, targetId, status)
70
-    // the url structure is
71
-    // const charmander = await db.get(`/profile/{profile_id}/queue/{target_id}/delete?include_profile=true&reinsert=false`)
72
-    // http://localhost:3001/api/profile/38/queue/9/delete?include_profile=true&reinsert=true
73
-    const profileId = props.pid
74
-    await updateQueueByProfileId(profileId, targetId, false)
75
-    const { membershipMatch, groupingName } = await postMembershipByProfileId({
76
-        profileId,
77
-        targetId,
78
-    })
79
-
80
-    // Reuse old grouping name if theres a match
81
-    let channel = groupingName
82
-    if (membershipMatch?.hasMatch) {
83
-        channel = membershipMatch.groupings[0].grouping_name
84
-    }
85
-    await subscribeToChannel(channel)
86
-    emit('reload')
87
-}
88
-
89
-const subscribeToChannel = async channelName => {
90
-    // create a chatter reference from the current profile
91
-    const chatter = currentProfile.chatter
92
-
93
-    /**
94
-     * publish a new message to the chatter with the channel and the message & title is optional
95
-     */
96
-    // You MUST send chatter channels as an array in an object
97
-    chatter.subscribe({ channels: [channelName] })
98
-    const res = await chatter.publish(channelName, {
99
-        title: 'New Message',
100
-        description: `This is the checking to see if we are subscribed to the ${channelName} channel!`,
101
-    })
102
-    // PubNub response will be a timecode of when the message was published
103
-    //router.push({ path: `/chat/${pid}` })
104
-}
105
-
106
-const pass = targetId => {
107
-    if (targetId == props.pid) return
108
-    updateQueueByProfileId(props.pid, targetId, true)
109
-    emit('reload')
110
-}
111 35
 </script>
112 36
 
113 37
 <style lang="sass">

+ 91
- 0
frontend/src/components/SpiderChart.vue Переглянути файл

@@ -0,0 +1,91 @@
1
+<template lang="pug">
2
+.spider-chart.w-flex.pt6.pb6
3
+    canvas#spider-chart-canvas
4
+</template>
5
+
6
+<script setup>
7
+import { Chart, registerables } from 'chart.js'
8
+import { onMounted } from 'vue'
9
+
10
+const props = defineProps({
11
+    profileName: {
12
+        required: true,
13
+        type: String,
14
+    },
15
+    profileData: {
16
+        required: true,
17
+        type: Object,
18
+    },
19
+    targetData: {
20
+        required: true,
21
+        type: Object,
22
+    },
23
+    labels: {
24
+        required: true,
25
+        type: Array,
26
+    },
27
+})
28
+
29
+const chartStyleDefaults = {
30
+    borderWidth: '1px',
31
+    fill: true,
32
+}
33
+const profile = {
34
+    label: props.profileName,
35
+    data: props.profileData,
36
+    backgroundColor: 'rgba(242, 205, 92, 0.3)',
37
+    borderColor: '#F2CD5C',
38
+    ...chartStyleDefaults,
39
+}
40
+const role = {
41
+    label: 'Role',
42
+    data: props.targetData,
43
+    backgroundColor: 'rgba(3, 136, 166, 0.30)',
44
+    borderColor: '#0388A6',
45
+    ...chartStyleDefaults,
46
+}
47
+
48
+const options = {
49
+    plugins: {
50
+        legend: {
51
+            position: 'bottom',
52
+            labels: {
53
+                color: '#FFFFFF',
54
+                boxWidth: 10,
55
+            },
56
+        },
57
+    },
58
+    scales: {
59
+        r: {
60
+            angleLines: {
61
+                color: '#FFFFFF',
62
+            },
63
+            grid: {
64
+                color: '#EAF0F4',
65
+                lineWidth: 1,
66
+            },
67
+            pointLabels: {
68
+                color: '#FFFFFF',
69
+            },
70
+            suggestedMax: 6,
71
+            ticks: {
72
+                display: false,
73
+                stepSize: 1.6,
74
+            },
75
+        },
76
+    },
77
+}
78
+
79
+onMounted(() => {
80
+    Chart.register(...registerables)
81
+    const ctx = document.getElementById('spider-chart-canvas')
82
+    new Chart(ctx, {
83
+        type: 'radar',
84
+        data: {
85
+            labels: props.labels,
86
+            datasets: [profile, role],
87
+        },
88
+        options,
89
+    })
90
+})
91
+</script>

+ 61
- 14
frontend/src/components/SummaryBar.vue Переглянути файл

@@ -1,51 +1,74 @@
1 1
 <template lang="pug">
2 2
 section.w-flex.column.pb5
3 3
     nav.fill-width.w-flex.column.justify-space-between
4
-
5 4
         // Tabbed Layout
6
-        w-tabs(v-if="isTab" :items="Object.keys(tabContent)" center fill-bar)
7
-            template(#item-title="{ item }")
5
+        w-tabs(
6
+            :items='Object.keys(tabContent)'
7
+            @input='onTabChanged'
8
+            center
9
+            fill-bar
10
+            v-if='isTab'
11
+        )
12
+            template(#item-title='{ item }')
8 13
                 .w-flex.column.justify-start
9
-                    p(v-if="tabContent[item].matchPerc") {{ tabContent[item].matchPerc }}%
14
+                    p(v-if='tabContent[item].matchPerc') {{ tabContent[item].matchPerc }}%
10 15
                     p(v-else) &nbsp;
11 16
                     p {{ item }}
12 17
             // About Tab
13
-            template(#item-content.1="{ item }")
18
+            template(#item-content.1='{ item }')
14 19
                 .tab--about
15 20
                     p {{ tabContent[item].tab }}
16 21
                     br
17 22
                     p {{ tabContent[item].tab }}
18 23
                     br
19 24
                     hr
20
-            
25
+
21 26
             // Passion Tab
22
-            template(#item-content.2="{ item }")
27
+            template(#item-content.2='{ item }')
23 28
                 .tab--passion
24 29
                     p {{ tabContent[item].tab }}
30
+                    SpiderChart(
31
+                        :labels='aspects.map(label => label.name)'
32
+                        :profile-data='profileScore'
33
+                        :target-data='targetScore'
34
+                        profile-name='lucy'
35
+                        v-if='isTab'
36
+                    )
25 37
 
26 38
             // Aspirations Tab
27
-            template(#item-content.3="{ item }")
39
+            template(#item-content.3='{ item }')
28 40
                 .tab--aspirations
29 41
                     p {{ tabContent[item].tab }}
30 42
 
31 43
             // Skills Tab
32
-            template(#item-content.4="{ item }")
44
+            template(#item-content.4='{ item }')
33 45
                 .tab--skills
34 46
                     p {{ tabContent[item].tab }}
35 47
 
36 48
         // Untabbed Layout
37 49
         ul.w-flex.row.justify-space-between(v-else)
38
-            template(v-for="(item, index) in Object.keys(tabContent)" :key="index")
39
-                li.w-flex.row(v-if="item !== 'about'")
40
-                    w-icon(xl).mr1 mdi mdi-heart
50
+            template(
51
+                :key='index'
52
+                v-for='(item, index) in Object.keys(tabContent)'
53
+            )
54
+                li.w-flex.row(v-if='item !== "about"')
55
+                    w-icon.mr1(xl) mdi mdi-heart
41 56
                     .w-flex.column.justify-start
42
-                        p {{ tabContent[item].matchPerc }}%
43
-                        p {{ item }}
57
+                        p 
58
+                            span {{ tabContent[item].matchPerc }}%
59
+                        p.text-capitalize {{ item }}
44 60
 </template>
45 61
 
46 62
 <script>
63
+import SpiderChart from './SpiderChart.vue'
64
+
47 65
 export default {
66
+    components: { SpiderChart },
48 67
     props: {
68
+        aspects: {
69
+            required: true,
70
+            type: Array,
71
+        },
49 72
         tabContent: {
50 73
             required: true,
51 74
             type: Object,
@@ -61,5 +84,29 @@ export default {
61 84
             default: true,
62 85
         },
63 86
     },
87
+    emits: ['tab-change'],
88
+    data: () => ({
89
+        profileScore: [5.7, 5.2, 4.8, 5.2, 4.9, 4.9],
90
+        targetScore: [5.3, 4.8, 5.7, 4.8, 5.6, 4.8],
91
+    }),
92
+    methods: {
93
+        onTabChanged(tabs) {
94
+            this.$emit('tab-change', tabs)
95
+        },
96
+    },
64 97
 }
65 98
 </script>
99
+<style lang="sass">
100
+    section
101
+        font-family: Century Gothic, CenturyGothic, AppleGothic, sans-serif
102
+        ul
103
+            margin: 11px 0
104
+            li
105
+                margin: 0 7px
106
+                font-size: .8em
107
+                p > span
108
+                    font-weight: bold
109
+                    font-size: 1em
110
+            li:not(:last-child)
111
+                border-right: 1px solid #fff
112
+</style>

+ 23
- 7
frontend/src/components/TagList.vue Переглянути файл

@@ -1,7 +1,6 @@
1 1
 <template lang="pug">
2
-section.w-flex.row.pb5
3
-    w-tag(v-for="tag in tags" color="pink-light1" bg-color="pink-light5").w-flex.grow.mr12.pa12
4
-        w-icon(v-if="icon" class="mr1" sm) {{ icon.family }} {{ icon.family }}-{{ icon.shape }}
2
+section.w-flex.row.wrap.pb5.justify-space-between
3
+    w-tag(v-for='tag in tags')
5 4
         p {{ tag }}
6 5
 </template>
7 6
 
@@ -9,12 +8,15 @@ section.w-flex.row.pb5
9 8
 export default {
10 9
     props: {
11 10
         tags: {
12
-            required: true,
11
+            required: false,
13 12
             type: Array,
14 13
             default: () => [
15
-                'tag one',
16
-                'tag another thing',
17
-                'tag something long',
14
+                'California RN License',
15
+                'BSN',
16
+                'Patient Happiness',
17
+                'ACLS',
18
+                'Strong Communication',
19
+                'High Volume',
18 20
             ],
19 21
         },
20 22
         icon: {
@@ -24,3 +26,17 @@ export default {
24 26
     data: () => ({}),
25 27
 }
26 28
 </script>
29
+
30
+<style lang="sass">
31
+section
32
+    min-height: 100%
33
+    .w-tag
34
+        background-color: #F7F5A6
35
+        margin: 3px 0
36
+        p
37
+            color: #183770
38
+            padding: 5px
39
+            font-size: 12px
40
+            font-weight: bolder
41
+            text-align: center
42
+</style>

+ 42
- 0
frontend/src/components/onboarding/AccountType.vue Переглянути файл

@@ -0,0 +1,42 @@
1
+<template lang="pug">
2
+w-card.w-flex.column
3
+    p {Name}, let's get started on your profile while we verify your account
4
+    w-button.search-type(@click='handleSubmit("Recruiter")')
5
+        w-icon.mr1 mdi mdi-account-multiple
6
+        | CANDIDATES
7
+    w-button.search-type(@click='handleSubmit("Jobseeker")')
8
+        w-icon.mr1 mdi mdi-bookmark-box-multiple
9
+        | JOBS
10
+</template>
11
+
12
+<script>
13
+export default {
14
+    name: 'AccountType',
15
+    props: {
16
+        currentStep: {
17
+            required: true,
18
+            type: Number,
19
+            default: 0,
20
+        },
21
+        aspectQuestions: {
22
+            required: false,
23
+        },
24
+    },
25
+    emits: ['go-to-step', 'update-answers'],
26
+    data: () => ({
27
+        question:
28
+            "{Name}, let's get started on your profile while we verify your account.",
29
+    }),
30
+    methods: {
31
+        handleSubmit(accountType) {
32
+            let payload = {
33
+                key: 'AccountType',
34
+                question: this.question,
35
+                answer: accountType,
36
+            }
37
+            this.$emit('update-answers', payload)
38
+            this.$emit('go-to-step', this.currentStep + 1)
39
+        },
40
+    },
41
+}
42
+</script>

+ 48
- 0
frontend/src/components/onboarding/Aspects.vue Переглянути файл

@@ -0,0 +1,48 @@
1
+<template lang="pug">
2
+w-card.aspects.w-flex.column
3
+    form.questionnaire(@submit.prevent='this.$emit("handle-submit")')
4
+        QuestionResponse(
5
+            :question='question'
6
+            @updated='updateRadio'
7
+            v-for='question in aspectQuestions'
8
+        )
9
+        w-button.ma1.grow(bg-color='success' type='submit')
10
+            w-icon.mr1 wi-check
11
+            | SUBMIT ANSWERS
12
+</template>
13
+
14
+<script>
15
+import QuestionResponse from './QuestionResponse.vue'
16
+const answered = [null, null, null, null, null, null]
17
+
18
+export default {
19
+    name: 'Aspects',
20
+    components: {
21
+        QuestionResponse,
22
+    },
23
+    props: {
24
+        aspectQuestions: {
25
+            required: true,
26
+            type: Array,
27
+        },
28
+    },
29
+    emits: ['handle-submit', 'update-answers'],
30
+    async created() {
31
+        this.aspectQuestions.forEach((q, i) => {
32
+            console.log(`Aspect #${i}: ${JSON.stringify(q)}`)
33
+        })
34
+    },
35
+    methods: {
36
+        updateRadio(onRadioSelect) {
37
+            answered[onRadioSelect.id - 1] = onRadioSelect.answer
38
+            this.$emit('update-answers', {
39
+                key: 'Aspects',
40
+                question: {
41
+                    response_key_prompt: 'aspects',
42
+                },
43
+                answer: answered,
44
+            })
45
+        },
46
+    },
47
+}
48
+</script>

+ 40
- 0
frontend/src/components/onboarding/CompanyID.vue Переглянути файл

@@ -0,0 +1,40 @@
1
+<template lang="pug">
2
+w-card.company-id.w-flex.column
3
+    h3 Company ID
4
+    p {{ question }}
5
+    input(
6
+        placeholder='Get this from your admin.'
7
+        type='text'
8
+        v-model='companyID'
9
+    )
10
+    w-button.next-btn(@click='handleSubmit') Next
11
+</template>
12
+
13
+<script>
14
+export default {
15
+    name: 'CompanyID',
16
+    props: {
17
+        currentStep: {
18
+            required: true,
19
+            type: Number,
20
+            default: 0,
21
+        },
22
+    },
23
+    emits: ['go-to-step', 'update-answers'],
24
+    data: () => ({
25
+        companyID: null,
26
+        question: 'PLEASE ENTER YOUR COMPANY ID#:',
27
+    }),
28
+    methods: {
29
+        handleSubmit() {
30
+            let payload = {
31
+                key: 'CompanyID',
32
+                question: this.question,
33
+                answer: this.companyID,
34
+            }
35
+            this.$emit('update-answers', payload)
36
+            this.$emit('go-to-step', this.currentStep + 1)
37
+        },
38
+    },
39
+}
40
+</script>

+ 41
- 0
frontend/src/components/onboarding/FormDropdown.vue Переглянути файл

@@ -0,0 +1,41 @@
1
+<template lang="pug">
2
+.role
3
+    h3 {{ question.response_key_category }}
4
+    p {{ question.response_key_prompt }}
5
+    w-select.mt4(:items='items' placeholder='i am' v-model='selection')
6
+    w-button.ma1.grow(@click='handleSubmit') NEXT
7
+</template>
8
+
9
+<script>
10
+export default {
11
+    name: 'FormDropdown',
12
+    props: {
13
+        question: {
14
+            required: true,
15
+            type: Object,
16
+        },
17
+    },
18
+    emits: ['update-answers'],
19
+    data: () => ({
20
+        selection: null,
21
+    }),
22
+    computed: {
23
+        items() {
24
+            return this.question.responses.map(res => ({ label: res }))
25
+        },
26
+    },
27
+    methods: {
28
+        handleSubmit() {
29
+            if (!this.selection) {
30
+                console.warn('Please select a role.')
31
+                return
32
+            }
33
+            let payload = {
34
+                question: this.question,
35
+                answer: this.selection,
36
+            }
37
+            this.$emit('update-answers', payload)
38
+        },
39
+    },
40
+}
41
+</script>

+ 36
- 0
frontend/src/components/onboarding/FormInput.vue Переглянути файл

@@ -0,0 +1,36 @@
1
+<template lang="pug">
2
+.form-input
3
+    h3 {{ question.response_key_category }}
4
+    p {{ question.response_key_prompt }}
5
+    input(placeholder='i am a little teapot' type='text' v-model='input')
6
+    w-button.ma1.grow(@click='handleSubmit') NEXT
7
+</template>
8
+<script>
9
+export default {
10
+    name: 'FormInput',
11
+    props: {
12
+        question: {
13
+            required: true,
14
+            type: Object,
15
+        },
16
+    },
17
+    emits: ['update-answers'],
18
+    data: () => ({
19
+        input: null,
20
+    }),
21
+    methods: {
22
+        handleSubmit() {
23
+            if(this.question.response_key_prompt === 'password') {
24
+                this.$emit('update-answers') // no password collection
25
+                return
26
+            }
27
+
28
+            let payload = {
29
+                question: this.question,
30
+                answer: this.input,
31
+            }
32
+            this.$emit('update-answers', payload)
33
+        },
34
+    },
35
+}
36
+</script>

+ 36
- 0
frontend/src/components/onboarding/FormTags.vue Переглянути файл

@@ -0,0 +1,36 @@
1
+<template lang="pug">
2
+.form-tags
3
+    h3 {{ question.response_key_category }}
4
+    p {{ question.response_key_prompt }}
5
+    DynamicTagList(:placeholder='"a tag"' :tags='tags')
6
+    w-button.ma1.grow(@click='handleSubmit') NEXT
7
+</template>
8
+<script>
9
+import DynamicTagList from '../DynamicTagList.vue'
10
+
11
+export default {
12
+    name: 'FormTags',
13
+    components: {
14
+        DynamicTagList,
15
+    },
16
+    props: {
17
+        question: {
18
+            required: true,
19
+            type: Object,
20
+        },
21
+    },
22
+    emits: ['update-answers'],
23
+    data: () => ({
24
+        tags: [],
25
+    }),
26
+    methods: {
27
+        handleSubmit() {
28
+            let payload = {
29
+                question: this.question,
30
+                answer: this.tags,
31
+            }
32
+            this.$emit('update-answers', payload)
33
+        },
34
+    },
35
+}
36
+</script>

+ 41
- 0
frontend/src/components/onboarding/Interests.vue Переглянути файл

@@ -0,0 +1,41 @@
1
+<template lang="pug">
2
+w-card.interests.w-flex.column
3
+    h3 Interests
4
+    p {{ question }}
5
+    w-button.next-btn(@click='handleSubmit') NEXT
6
+</template>
7
+
8
+<script>
9
+import DynamicTagList from '../DynamicTagList.vue'
10
+
11
+export default {
12
+    name: 'Interests',
13
+    components: {
14
+        DynamicTagList,
15
+    },
16
+    props: {
17
+        currentStep: {
18
+            required: true,
19
+            type: Number,
20
+            default: 0,
21
+        },
22
+    },
23
+    emits: ['go-to-step', 'update-answers'],
24
+    data: () => ({
25
+        interests: [],
26
+        question:
27
+            'What are some interests you would like in your next candidates?',
28
+    }),
29
+    methods: {
30
+        handleSubmit() {
31
+            let payload = {
32
+                key: 'Interests',
33
+                question: this.question,
34
+                answer: this.interests,
35
+            }
36
+            this.$emit('update-answers', payload)
37
+            this.$emit('go-to-step', this.currentStep + 1)
38
+        },
39
+    },
40
+}
41
+</script>

+ 44
- 0
frontend/src/components/onboarding/LicensesAndCertifications.vue Переглянути файл

@@ -0,0 +1,44 @@
1
+<template lang="pug">
2
+w-card.licenses-certifications.w-flex.column
3
+    h3 Licenses & Certifications
4
+    p {{ question }}
5
+    DynamicTagList(
6
+        :placeholder='"requirement"'
7
+        :tags='licensesAndCertifications'
8
+    )
9
+    w-button.next-btn(@click='handleSubmit') NEXT
10
+</template>
11
+
12
+<script>
13
+import DynamicTagList from '../DynamicTagList.vue'
14
+
15
+export default {
16
+    name: 'LicensesAndCertifications',
17
+    components: {
18
+        DynamicTagList,
19
+    },
20
+    props: {
21
+        currentStep: {
22
+            required: true,
23
+            type: Number,
24
+            default: 0,
25
+        },
26
+    },
27
+    emits: ['go-to-step', 'update-answers'],
28
+    data: () => ({
29
+        licensesAndCertifications: [],
30
+        question: 'Are there any licenses and certification requirements?',
31
+    }),
32
+    methods: {
33
+        handleSubmit() {
34
+            let payload = {
35
+                key: 'LicensesAndCertifications',
36
+                question: this.question,
37
+                answer: this.licensesAndCertifications,
38
+            }
39
+            this.$emit('update-answers', payload)
40
+            this.$emit('go-to-step', this.currentStep + 1)
41
+        },
42
+    },
43
+}
44
+</script>

+ 40
- 0
frontend/src/components/onboarding/Location.vue Переглянути файл

@@ -0,0 +1,40 @@
1
+<template lang="pug">
2
+w-card.location.w-flex.column
3
+    h3 Location
4
+    p {{ question }}
5
+    DynamicTagList(:placeholder='"location"' :tags='locations')
6
+    w-button.next-btn(@click='handleSubmit') NEXT
7
+</template>
8
+
9
+<script>
10
+import DynamicTagList from '../DynamicTagList.vue'
11
+export default {
12
+    name: 'Location',
13
+    components: {
14
+        DynamicTagList,
15
+    },
16
+    props: {
17
+        currentStep: {
18
+            required: true,
19
+            type: Number,
20
+            default: 0,
21
+        },
22
+    },
23
+    emits: ['go-to-step', 'update-answers'],
24
+    data: () => ({
25
+        locations: [],
26
+        question: 'Where would you like to select your candidates from?',
27
+    }),
28
+    methods: {
29
+        handleSubmit() {
30
+            let payload = {
31
+                key: 'Location',
32
+                question: this.question,
33
+                answer: this.locations,
34
+            }
35
+            this.$emit('update-answers', payload)
36
+            this.$emit('go-to-step', this.currentStep + 1)
37
+        },
38
+    },
39
+}
40
+</script>

frontend/src/components/QuestionResponse.vue → frontend/src/components/onboarding/QuestionResponse.vue Переглянути файл

@@ -1,10 +1,9 @@
1 1
 <template lang="pug">
2
-.question.pa12
3
-    header.w-flex.row.justify-space-between.pb6
4
-        label {{question.question}}
2
+w-card.question
3
+    p {{question.question}} 
5 4
     section.radio-buttons.w-flex.row.justify-space-between
6
-        h3(v-for="label in question.labels") {{label}}
7
-    w-radios.w-flex.row.justify-space-between(@update:model-value="onUpdate" :items="radioItems")
5
+        p(v-for="label in question.labels") {{label}}
6
+    w-radios.w-flex.row.justify-space-between(@update:model-value="onUpdate" :items="radioItems" color="red")
8 7
 </template>
9 8
 
10 9
 <script>

+ 40
- 0
frontend/src/components/onboarding/Role.vue Переглянути файл

@@ -0,0 +1,40 @@
1
+<template lang="pug">
2
+w-card.role.w-flex.column
3
+    h3 Actively Searching
4
+    p {{ question }}
5
+    w-select.mt4(:items='items' placeholder='i am' v-model='selectedRole')
6
+    w-button.next-btn(@click='handleSubmit') NEXT
7
+</template>
8
+
9
+<script>
10
+export default {
11
+    name: 'Role',
12
+    props: {
13
+        currentStep: {
14
+            required: true,
15
+            type: Number,
16
+            default: 0,
17
+        },
18
+    },
19
+    data: () => ({
20
+        items: [{ label: 'RECRUITER' }, { label: 'HIRING MANAGER' }],
21
+        question: 'What is your role at your company?',
22
+        selectedRole: null,
23
+    }),
24
+    methods: {
25
+        handleSubmit() {
26
+            if (!this.selectedRole) {
27
+                console.warn('Please select a role.')
28
+                return
29
+            }
30
+            let payload = {
31
+                key: 'Role',
32
+                question: this.question,
33
+                answer: this.selectedRole,
34
+            }
35
+            this.$emit('update-answers', payload)
36
+            this.$emit('go-to-step', this.currentStep + 1)
37
+        },
38
+    },
39
+}
40
+</script>

+ 41
- 0
frontend/src/components/onboarding/Skills.vue Переглянути файл

@@ -0,0 +1,41 @@
1
+<template lang="pug">
2
+w-card.skills.w-flex.column
3
+    h3 Recruiter
4
+    p {{ question }}
5
+    DynamicTagList(:placeholder='"skill"' :tags='skills')
6
+    w-button.next-btn(@click='handleSubmit') NEXT
7
+</template>
8
+
9
+<script>
10
+import DynamicTagList from '../DynamicTagList.vue'
11
+
12
+export default {
13
+    name: 'Skills',
14
+    components: {
15
+        DynamicTagList,
16
+    },
17
+    props: {
18
+        currentStep: {
19
+            required: true,
20
+            type: Number,
21
+            default: 0,
22
+        },
23
+    },
24
+    emits: ['go-to-step', 'update-answers'],
25
+    data: () => ({
26
+        question: 'What are some skills you are looking for at your company?',
27
+        skills: [],
28
+    }),
29
+    methods: {
30
+        handleSubmit() {
31
+            let payload = {
32
+                key: 'Skills',
33
+                question: this.question,
34
+                answer: this.skills,
35
+            }
36
+            this.$emit('update-answers', payload)
37
+            this.$emit('go-to-step', this.currentStep + 1)
38
+        },
39
+    },
40
+}
41
+</script>

+ 42
- 0
frontend/src/components/onboarding/Splash.vue Переглянути файл

@@ -0,0 +1,42 @@
1
+<template lang="pug">
2
+w-flex.column
3
+    w-image.splash-logo(
4
+        :height='300'
5
+        :src='`/assets/logos/siimee_logo.jpg`'
6
+        :width='300'
7
+    )
8
+    w-button.ma1.grow.next-btn(
9
+        :height='50'
10
+        :width='315'
11
+        @click='this.$emit("go-to-step", currentStep + 1)'
12
+        bg-color='success'
13
+        shadow
14
+        text
15
+        xl
16
+    ) GET STARTED
17
+</template>
18
+
19
+<script>
20
+export default {
21
+    name: 'Splash',
22
+    props: {
23
+        currentStep: {
24
+            required: true,
25
+            type: Number,
26
+            default: 0,
27
+        },
28
+    },
29
+    emits: ['update-answers'],
30
+    methods: {
31
+        handleSubmit() {
32
+            this.$emit('update-answers', null)
33
+        },
34
+    },
35
+}
36
+</script>
37
+
38
+<style lang="sass">
39
+.w-button
40
+    &.next-btn
41
+        background-color: #5BA626
42
+</style>

+ 27
- 0
frontend/src/components/onboarding/index.js Переглянути файл

@@ -0,0 +1,27 @@
1
+import Splash from './Splash.vue'
2
+import AccountType from './AccountType.vue'
3
+import CompanyID from './CompanyID.vue'
4
+import Role from './Role.vue'
5
+import Skills from './Skills.vue'
6
+import Location from './Location.vue'
7
+import Interests from './Interests.vue'
8
+import LicensesAndCertifications from './LicensesAndCertifications.vue'
9
+import Aspects from './Aspects.vue'
10
+import FormInput from './FormInput.vue'
11
+import FormTags from './FormTags.vue'
12
+import FormDropdown from './FormDropdown.vue'
13
+
14
+export default {
15
+    Splash,
16
+    AccountType,
17
+    CompanyID,
18
+    Role,
19
+    Skills,
20
+    Location,
21
+    Interests,
22
+    LicensesAndCertifications,
23
+    Aspects,
24
+    FormDropdown,
25
+    FormTags,
26
+    FormInput,
27
+}

+ 36
- 33
frontend/src/entities/card/card.js Переглянути файл

@@ -1,7 +1,7 @@
1 1
 /** @module card/card */
2 2
 
3 3
 const DEFAULT_ABOUT =
4
-    'A really really really really really. Really really really really really really really really really really really long bio.'
4
+    'Hello! My name is L.L. and I am a nurse from New York. I have been in the healthcare industry for over 6 years.'
5 5
 
6 6
 class SummaryGroup {
7 7
     constructor() {
@@ -46,6 +46,32 @@ class Aspect {
46 46
         this.percentage = percentage
47 47
     }
48 48
 }
49
+const cardAspects = [
50
+    new Aspect({
51
+        name: 'creativity',
52
+        labels: ['creative', 'methodical'],
53
+    }),
54
+    new Aspect({
55
+        name: 'dynamism',
56
+        labels: ['dynamic', 'ordered'],
57
+    }),
58
+    new Aspect({
59
+        name: 'precision',
60
+        labels: ['precise', 'resourceful'],
61
+    }),
62
+    new Aspect({
63
+        name: 'vision',
64
+        labels: ['visionary', 'implementer'],
65
+    }),
66
+    new Aspect({
67
+        name: 'focus',
68
+        labels: ['big picture', 'focused'],
69
+    }),
70
+    new Aspect({
71
+        name: 'attention',
72
+        labels: ['guided', 'self-managed'],
73
+    }),
74
+]
49 75
 
50 76
 /**
51 77
  * Class representing a profile card
@@ -54,52 +80,28 @@ class Aspect {
54 80
  * card facade
55 81
  */
56 82
 class Card {
57
-    constructor({ pid, name, role }) {
83
+    constructor({ pid, name, email, role }) {
58 84
         this.pid = pid
59 85
 
60 86
         /**  Fields */
61 87
         this.name = name
62 88
 
63
-        this.role = role ? role : null
89
+        this.role = role ? role : 'registered nurse'
64 90
 
65 91
         this.presence = null
66 92
         this.urgency = null
67 93
         this.pronouns = 'she/her/hers'
68
-        this.ethinicity = null
69
-        this.locale = null
70
-        this.email = null
94
+        this.ethinicity = 'something'
95
+        this.locale = 'los angeles ca'
96
+        this.email = email
71 97
 
72 98
         this.images = []
73 99
         this.tags = []
100
+        this.reveal = {}
74 101
 
75 102
         this.summary = new SummaryGroup()
76 103
 
77
-        this.aspects = [
78
-            new Aspect({
79
-                name: 'creativity',
80
-                labels: ['creative', 'methodical'],
81
-            }),
82
-            new Aspect({
83
-                name: 'dynamism',
84
-                labels: ['dynamic', 'ordered'],
85
-            }),
86
-            new Aspect({
87
-                name: 'precision',
88
-                labels: ['precise', 'resourceful'],
89
-            }),
90
-            new Aspect({
91
-                name: 'vision',
92
-                labels: ['visionary', 'implementer'],
93
-            }),
94
-            new Aspect({
95
-                name: 'focus',
96
-                labels: ['big picture', 'focused'],
97
-            }),
98
-            new Aspect({
99
-                name: 'attention',
100
-                labels: ['guided', 'self-managed'],
101
-            }),
102
-        ]
104
+        this.aspects = cardAspects
103 105
 
104 106
         return this
105 107
     }
@@ -109,6 +111,7 @@ const makeCardFromProfile = profile => {
109 111
     const c = new Card({
110 112
         pid: profile.profile_id,
111 113
         name: profile.user_name,
114
+        email: profile.user_email,
112 115
         role: profile?.profile_prefs?.role?.val,
113 116
     })
114 117
 
@@ -128,4 +131,4 @@ const makeCardFromProfile = profile => {
128 131
     }
129 132
     return c
130 133
 }
131
-export { Card, makeCardFromProfile }
134
+export { Card, makeCardFromProfile, cardAspects }

+ 43
- 0
frontend/src/entities/grouping/grouping.js Переглянути файл

@@ -1,7 +1,9 @@
1 1
 /** @module entities/grouping */
2 2
 
3
+import { ref } from 'vue'
3 4
 import { _baseRecord } from '../index.js'
4 5
 import { groupingSchema } from './grouping.schema.js'
6
+import { revealProfileInfo } from '../../services/profile.service.js'
5 7
 
6 8
 /** Class representing a grouping */
7 9
 class Grouping extends _baseRecord {
@@ -13,14 +15,55 @@ class Grouping extends _baseRecord {
13 15
      */
14 16
     constructor({ ...membership }) {
15 17
         super()
18
+        this._loading = ref(true)
16 19
 
17 20
         this.type = this.constructor.name.toLowerCase()
21
+        this.tags = []
18 22
 
19 23
         /** Pass destructured data to the module system */
20 24
         Object.assign(this, membership)
21 25
 
22 26
         return this
23 27
     }
28
+    get revealed() {
29
+        const revealed = {}
30
+        this.tags
31
+            .filter(assoc => assoc.tag.tag_category == 'reveal')
32
+            .forEach(assoc => {
33
+                if (!revealed[assoc.profile_id]) {
34
+                    revealed[assoc.profile_id] = []
35
+                }
36
+                revealed[assoc.profile_id].push({
37
+                    description: assoc.tag.tag_description,
38
+                })
39
+            })
40
+        return revealed
41
+    }
42
+    get participants() {
43
+        return [this.profile.profile_id]
44
+    }
45
+    async reveal(profileId, tagId) {
46
+        console.log('[Grouping Entity log]: Attempting to reveal...', tagId)
47
+        this._loading.value = true
48
+        try {
49
+            const revealed = await revealProfileInfo(
50
+                this.grouping_id,
51
+                profileId,
52
+                tagId,
53
+            )
54
+            if (!revealed.tags) {
55
+                throw `[Grouping Entity error]: Could not reveal ${tagId} for ${profileId}`
56
+            }
57
+            // Overwrite with new revealed tags completely
58
+            const toKeep = this.tags.filter(
59
+                assoc => assoc.profile_id != profileId,
60
+            )
61
+            this.tags = [...toKeep, ...revealed.tags]
62
+        } catch (err) {
63
+            console.error(err)
64
+        }
65
+        this._loading.value = false
66
+    }
24 67
     /**
25 68
      * validate this record
26 69
      * @return {boolean} is it valid or not?

+ 4
- 0
frontend/src/entities/grouping/grouping.schema.js Переглянути файл

@@ -10,11 +10,15 @@ import Joi from 'joi'
10 10
 const groupingSchema = {
11 11
     type: 'object',
12 12
     properties: Joi.object().keys({
13
+        /** for vue reactivity */
14
+        _loading: Joi.object(),
15
+
13 16
         /** _baseRecord fields */
14 17
         createdAt: Joi.string(),
15 18
         _id: Joi.string(),
16 19
         lastUpdatedAt: Joi.string(),
17 20
         type: Joi.string(),
21
+        tags: Joi.array(),
18 22
 
19 23
         /** our fields */
20 24
         grouping_id: Joi.number().required(),

+ 1
- 1
frontend/src/entities/profile/profile.js Переглянути файл

@@ -36,7 +36,7 @@ class Profile extends _baseRecord {
36 36
          * TODO: Send validate.error to logging error handler
37 37
          */
38 38
         if (validate.error) {
39
-            console.error(`error: ${validate.error} - ${this.type} validation`)
39
+            console.error(`[Profile Entitiy error]: ${validate.error}`)
40 40
         }
41 41
 
42 42
         /** validate(this) always returns something so force it to a bool */

+ 28
- 10
frontend/src/entities/survey/survey.js Переглянути файл

@@ -2,24 +2,42 @@
2 2
 import { _baseRecord } from '../index.js'
3 3
 import { surveySchema } from './survey.schema.js'
4 4
 
5
+const SCORED = [1, 2, 3, 4, 5, 6]
6
+const _isScored = id => SCORED.includes(id)
7
+const _makeCategoryFriendly = responseCategory => {
8
+    const labels = responseCategory.split('_vs_')
9
+    labels.forEach((a, i) => {
10
+        if (a.indexOf('_') == -1) return
11
+        labels[i] = a.split('_').join(' ')
12
+    })
13
+    return labels
14
+}
15
+const _formatAspectQuestions = steps => {
16
+    return steps
17
+        .map(q => {
18
+            if (!_isScored(q.response_key_id)) return null
19
+            return {
20
+                id: q.response_key_id,
21
+                question: q.response_key_prompt,
22
+                labels: _makeCategoryFriendly(q.response_key_category),
23
+                answer: null,
24
+            }
25
+        })
26
+        .filter(step => step != null)
27
+}
28
+
5 29
 class Survey extends _baseRecord {
6
-    constructor(questionSteps, roles) {
30
+    constructor(questionSteps) {
7 31
         super()
8 32
 
9 33
         this.type = this.constructor.name.toLowerCase()
10 34
 
11 35
         /**  Fields */
12 36
         this.steps = [...questionSteps] // ! required
13
-        this.roleTree = roles
14
-
15
-        return this
16
-    }
17
-    setRoleResponses(position) {
18
-        const roleStep = this.steps.filter(
19
-            step => step.response_key_prompt == 'role',
20
-        )[0]
21
-        roleStep.responses = this.roleTree[position]
37
+        this.aspectQuestions = _formatAspectQuestions(this.steps)
38
+        console.log('this.aspectQuestions: ', JSON.stringify(this.aspectQuestions))
22 39
     }
40
+
23 41
     isValid() {
24 42
         const validate = surveySchema.validate(this)
25 43
 

+ 2
- 1
frontend/src/main.js Переглянути файл

@@ -8,7 +8,8 @@ import components from './wave'
8 8
 import App from './App.vue'
9 9
 import MainNav from './components/MainNav.vue'
10 10
 
11
-const DEV = import.meta.env.VITE_DEV == 'true'
11
+// const DEV = import.meta.env.VITE_DEV == 'true'
12
+const DEV = false
12 13
 
13 14
 /**
14 15
  * Check between route changes for login/timeout

+ 20
- 8
frontend/src/router/guards.js Переглянути файл

@@ -1,17 +1,29 @@
1 1
 import { currentProfile } from '../services'
2 2
 
3
+const DEV_MODE = import.meta.env.VITE_DEV == 'true'
3 4
 
4
-const checkLoginStatus = (destination, nextCb) => {
5
-    if(!currentProfile.isLoggedIn || !currentProfile.isComplete) {
6
-        console.warn(`profile: ${currentProfile.id.value} | login: ${currentProfile.isLoggedIn} | completed: ${currentProfile.isComplete}`)
5
+async function log(to) {
6
+    if (DEV_MODE) {
7
+        if (!currentProfile.isLoggedIn || !currentProfile.isComplete) {
8
+            console.info(
9
+                `[Guard Status debug]: Profile: ${currentProfile.id.value} | Login: ${currentProfile.isLoggedIn} | Complete: ${currentProfile.isComplete}`,
10
+            )
11
+        }
12
+        console.info('[Guard Status debug]: being routed to:', to.fullPath)
7 13
     }
8
-    if (
14
+}
15
+
16
+const checkLoginStatus = (destination, nextCb) => {
17
+    log(destination)
18
+    if (DEV_MODE) {
19
+        nextCb()
20
+    } else if (
9 21
         destination.meta.requiresCompleteProfile &&
10 22
         !currentProfile.isLoggedIn &&
11
-        !currentProfile.isComplete 
23
+        !currentProfile.isComplete
12 24
     ) {
13
-        nextCb('survey')
14
-    } else if(
25
+        nextCb('onboarding')
26
+    } else if (
15 27
         destination.meta.requiresCompleteProfile &&
16 28
         destination.meta.requiresAuth &&
17 29
         !currentProfile.isLoggedIn
@@ -22,4 +34,4 @@ const checkLoginStatus = (destination, nextCb) => {
22 34
     }
23 35
 }
24 36
 
25
-export { checkLoginStatus }
37
+export { checkLoginStatus }

+ 12
- 0
frontend/src/router/index.js Переглянути файл

@@ -69,6 +69,18 @@ const routes = [
69 69
         name: `LoginView`,
70 70
         meta: { requiresAuth: false, requiresCompleteProfile: false },
71 71
     },
72
+    {
73
+        path: `/settings`,
74
+        component: HomeView,
75
+        name: `SettingsView`,
76
+        meta: { requiresAuth: true, requiresCompleteProfile: true },
77
+    },
78
+    {
79
+        path: `/search`,
80
+        component: HomeView,
81
+        name: `SearchView`,
82
+        meta: { requiresAuth: true, requiresCompleteProfile: true },
83
+    },
72 84
 ]
73 85
 
74 86
 const router = createRouter({

+ 30
- 4
frontend/src/services/chat.service.js Переглянути файл

@@ -101,6 +101,31 @@ class Chatter {
101 101
         this.subscribe({ channels: this._subscriptions })
102 102
         return this.subscriptions
103 103
     }
104
+    async getHistory(channel, count = 10) {
105
+        console.warn('[chatter] grabbing history for channel:', channel)
106
+        let pastMessages = null
107
+        try {
108
+            pastMessages = await this.provider.fetchMessages({
109
+                channels: [channel],
110
+                count: count,
111
+                includeMessageType: true,
112
+                includeUUID: true,
113
+                includeMeta: true,
114
+                includeMessageActions: false,
115
+            })
116
+        } catch (error) {
117
+            console.error('[chatter]', error)
118
+        }
119
+        const channelHistory = pastMessages.channels[channel]
120
+        console.log('channelHistory :>> ', channelHistory)
121
+        return channelHistory
122
+            ? channelHistory.map(msg => ({
123
+                  publisher: msg.uuid,
124
+                  message: new ChatMessage(msg.message.description),
125
+                  timetoken: msg.timetoken,
126
+              }))
127
+            : []
128
+    }
104 129
     /**
105 130
      * Send a message to a channel
106 131
      * example = new ChatMessage({ title: 'example', description: 'ni' })
@@ -118,10 +143,9 @@ class Chatter {
118 143
     /**
119 144
      * Subscribe to a channels
120 145
      * Facade so we can hide provider specific methods
121
-     * @param {array} channels
146
+     * @param {array} channels grouping name
122 147
      */
123 148
     subscribe({ channels }) {
124
-        console.log('subscribing to:', channels)
125 149
         _providerMethods.subscribe({ channels })
126 150
     }
127 151
     /**
@@ -131,8 +155,10 @@ class Chatter {
131 155
     _listenFor({ listeners }) {
132 156
         _providerMethods.listen(listeners)
133 157
     }
134
-    //  step 2: build the this.subscriptions array from the this.groupings object
135
-    // fetch all groupings for this profile and then store them in the chatter groupings object for reference
158
+    /**
159
+     * Get all groupings for this profile and
160
+     * then store them as subscriptions
161
+     */
136 162
     async _setupAllChannels(groupings) {
137 163
         groupings.forEach(grouping => {
138 164
             this._subscriptions.push(grouping.grouping_name)

+ 35
- 10
frontend/src/services/grouping.service.js Переглянути файл

@@ -1,5 +1,5 @@
1
-import { db } from '../utils/db'
2
-import { Grouping, Profile } from '../entities'
1
+import { db } from '../utils/db.js'
2
+import { Grouping, Profile } from '../entities/index.js'
3 3
 
4 4
 /**
5 5
  * Get Memberships associated with a single Profile from the database and
@@ -9,15 +9,28 @@ import { Grouping, Profile } from '../entities'
9 9
  * @returns {array} instantiated Profile objects (see: /entites/profile)
10 10
  */
11 11
 const fetchMembershipsByProfileId = async profileId => {
12
-    const membershipsForProfileId = await db.get(`/membership/${profileId}`)
13 12
     const validGroupingInstances = []
14
-    for (let membership of membershipsForProfileId) {
15
-        const grouping = new Grouping(membership)
16
-        if (grouping.isValid()) {
17
-            // Reformat incoming profile data into Profile entity
18
-            grouping.profile = new Profile(grouping.profile)
19
-            validGroupingInstances.push(grouping)
13
+    let memberships
14
+    try {
15
+        memberships = await db.get(`/membership/${profileId}`)
16
+        for (let membership of memberships) {
17
+            const grouping = new Grouping(membership)
18
+            if (grouping.isValid()) {
19
+                // Reformat incoming profile data into Profile entity
20
+                grouping.profile = new Profile(grouping.profile)
21
+                const targetTags = await db.get(
22
+                    `/profile/${grouping.profile.profile_id}/tags/${grouping.grouping_id}`,
23
+                )
24
+                const profileTags = await db.get(
25
+                    `/profile/${profileId}/tags/${grouping.grouping_id}`,
26
+                )
27
+                grouping.tags = [...targetTags, ...profileTags]
28
+                grouping._loading.value = false
29
+                validGroupingInstances.push(grouping)
30
+            }
20 31
         }
32
+    } catch (error) {
33
+        console.error(`[Grouping Service]: ${error}\ngroupings: ${memberships}`)
21 34
     }
22 35
     return validGroupingInstances
23 36
 }
@@ -46,4 +59,16 @@ const postMembershipByProfileId = async ({
46 59
     )
47 60
     return { membershipMatch, groupingName: membership.grouping_name }
48 61
 }
49
-export { fetchMembershipsByProfileId, postMembershipByProfileId }
62
+
63
+const revealProfileInfo = async (membershipId, profileId, tagId) => {
64
+    const revealed = await db.post(
65
+        `/membership/${membershipId}/reveal?profile=${profileId}&tag=${tagId}`,
66
+    )
67
+    return revealed
68
+}
69
+
70
+export {
71
+    fetchMembershipsByProfileId,
72
+    postMembershipByProfileId,
73
+    revealProfileInfo,
74
+}

+ 8
- 9
frontend/src/services/index.js Переглянути файл

@@ -1,9 +1,8 @@
1
-export * from './user.service'
2
-export * from './profile.service'
3
-export * from './grouping.service'
4
-export * from './survey.service'
5
-export * from './queue.service'
6
-export * from './chat.service'
7
-export * from './notification.service'
8
-export * from './login.service'
9
-
1
+export * from './user.service.js'
2
+export * from './profile.service.js'
3
+export * from './grouping.service.js'
4
+export * from './survey.service.js'
5
+export * from './queue.service.js'
6
+export * from './chat.service.js'
7
+export * from './notification.service.js'
8
+export * from './login.service.js'

+ 46
- 13
frontend/src/services/login.service.js Переглянути файл

@@ -1,11 +1,14 @@
1 1
 import { ref } from 'vue'
2 2
 import {
3
+    fetchQueueByProfileId,
4
+    updateQueueByProfileId,
3 5
     fetchMembershipsByProfileId,
4 6
     fetchResponsesByProfileId,
7
+    fetchProfileByProfileId,
5 8
     Chatter,
6 9
     StonkAlert,
7
-} from '../services'
8
-import { surveyFactory } from '../utils'
10
+} from '../services/index.js'
11
+import { surveyFactory } from '../utils/index.js'
9 12
 
10 13
 /**
11 14
  * Logged in profile state manager
@@ -15,10 +18,13 @@ class Login {
15 18
     constructor() {
16 19
         this._loading = ref(true)
17 20
 
21
+        // Profile entity instance for self
22
+        this._profile = null
18 23
         // Make reactive with vue observer
19 24
         this.id = ref(null)
20 25
 
21 26
         this.groupings = []
27
+        this.queue = []
22 28
         this.responses = []
23 29
         this.tags = []
24 30
 
@@ -72,22 +78,41 @@ class Login {
72 78
      */
73 79
     async login(profileId, cb) {
74 80
         this._loading.value = true
81
+        // First check if profile exists
82
+        console.warn('[Login Service warn]: Logging in:', profileId)
75 83
 
76
-        console.warn('logging in:', profileId)
77
-        this.id.value = parseInt(profileId)
84
+        // TODO: You can probably use this call to get responses, groupings and tags
85
+        this._profile = await fetchProfileByProfileId(profileId)
86
+        this.id.value = this._profile.profile_id
78 87
 
88
+        if (!this.id.value) {
89
+            console.error(
90
+                `[Login Service error]: No profile found for profile_id: ${profileId}`,
91
+            )
92
+        }
93
+
94
+        // Then grab groupings and queue for display
79 95
         await this.getGroupings()
96
+        await this.getQueue()
80 97
 
81
-        await this.setupChatter()
98
+        // Then hook into chat services
99
+        try {
100
+            await this.setupChatter()
101
+            console.warn(
102
+                `[Login Service warn]: ${profileId} subscribed to:`,
103
+                this.chatter.subscriptions,
104
+            )
105
+        } catch (err) {
106
+            console.error(err)
107
+        }
108
+        // Finally setup notifications and sse handling
82 109
         this.setupToaster(cb)
83
-
110
+        console.warn('[Login Service warn]: Login SUCCESSFUL')
84 111
         this._loading.value = false
85
-        console.warn('logged in:', this.isLoggedIn)
86
-        console.warn('subscribed to:', this.chatter.subscriptions)
87 112
         return this.id.value
88 113
     }
89 114
     logout() {
90
-        console.warn('logging out:', this.id.value)
115
+        console.warn('[Login Service warn]: Logging out:', this.id.value)
91 116
         this.id.value = null
92 117
         if (this.toaster) {
93 118
             this.toaster.stop()
@@ -102,7 +127,7 @@ class Login {
102 127
             const tags = []
103 128
             this.setTags(tags)
104 129
         } catch (err) {
105
-            console.error(err)
130
+            console.error(`[Login Service]: ${err}`)
106 131
         }
107 132
     }
108 133
     setTags(tags) {
@@ -114,7 +139,7 @@ class Login {
114 139
             const responseList = await fetchResponsesByProfileId(this.id.value)
115 140
             this.setResponses(responseList)
116 141
         } catch (err) {
117
-            console.error(err)
142
+            console.error(`[Login Service]: ${err}`)
118 143
         }
119 144
     }
120 145
     setResponses(responses) {
@@ -125,9 +150,17 @@ class Login {
125 150
         try {
126 151
             this.groupings = await fetchMembershipsByProfileId(this.id.value)
127 152
         } catch (err) {
128
-            console.error(err)
153
+            console.error(`[Login Service]: ${err}`)
129 154
         }
130 155
     }
156
+
157
+    async getQueue() {
158
+        this.queue = await fetchQueueByProfileId(this.id.value)
159
+    }
160
+    async updateQueue(profileId, targetId, reinsert) {
161
+        this.queue = await updateQueueByProfileId(profileId, targetId, reinsert)
162
+    }
163
+
131 164
     /**
132 165
      * For push notifications and chat
133 166
      */
@@ -143,4 +176,4 @@ class Login {
143 176
 
144 177
 const currentProfile = new Login()
145 178
 
146
-export { currentProfile }
179
+export { currentProfile, Login }

+ 1
- 1
frontend/src/services/notification.service.js Переглянути файл

@@ -1,4 +1,4 @@
1
-import { remote } from '../utils/db'
1
+import { remote } from '../utils/db.js'
2 2
 
3 3
 /**
4 4
  * Base notifier class

+ 24
- 4
frontend/src/services/profile.service.js Переглянути файл

@@ -1,5 +1,5 @@
1
-import { db } from '../utils/db'
2
-import { Profile } from '../entities/profile'
1
+import { db } from '../utils/db.js'
2
+import { Profile } from '../entities/profile/profile.js'
3 3
 
4 4
 /**
5 5
  * Get Profiles associated with a single user from the database and
@@ -26,8 +26,28 @@ const createProfileForUserId = async (userId, responses) => {
26 26
 }
27 27
 
28 28
 const fetchProfileByProfileId = async profileId => {
29
-    const profile = await db.get(`/profile/${profileId}`)
29
+    let profile
30
+    try {
31
+        const profileData = await db.get(`/profile/${profileId}`)
32
+        profile = new Profile(profileData)
33
+        if (!profile.isValid()) {
34
+            throw '[Profile Service error]: Invalid or incomplete profile returned.'
35
+        }
36
+    } catch (err) {
37
+        console.error(err)
38
+    }
30 39
     return profile
31 40
 }
32 41
 
33
-export { fetchProfilesByUserId, fetchProfileByProfileId, createProfileForUserId }
42
+const revealProfileInfo = async (groupingId, profileId, tagId) => {
43
+    const revealed = await db.post(
44
+        `/membership/${groupingId}/reveal?profile_id=${profileId}&tag_id=${tagId}`,
45
+    )
46
+    return revealed
47
+}
48
+export {
49
+    fetchProfilesByUserId,
50
+    fetchProfileByProfileId,
51
+    createProfileForUserId,
52
+    revealProfileInfo,
53
+}

+ 14
- 8
frontend/src/services/queue.service.js Переглянути файл

@@ -1,5 +1,5 @@
1
-import { db } from '../utils/db'
2
-import { Profile } from '../entities'
1
+import { db } from '../utils/db.js'
2
+import { Profile } from '../entities/index.js'
3 3
 
4 4
 /**
5 5
  * Get a match queue of profiles
@@ -8,16 +8,14 @@ import { Profile } from '../entities'
8 8
  */
9 9
 const fetchQueueByProfileId = async profileId => {
10 10
     let queue
11
-
12 11
     try {
13 12
         queue = await db.get(`/profile/${profileId}/queue?include_profile=true`)
14 13
         if (!queue?.length) {
15
-            throw 'Could not retrieve match queue. Please take the survey and rescore.'
14
+            throw '[Queue Service]: Could not retrieve match queue.\nHave you taken the survey and scored this profile?'
16 15
         }
17 16
     } catch (err) {
18 17
         console.error(err)
19 18
     }
20
-
21 19
     return queue
22 20
         ? queue.map(profileData => {
23 21
               return new Profile({ email: 'fixme@gmail.com', ...profileData })
@@ -33,9 +31,17 @@ const fetchQueueByProfileId = async profileId => {
33 31
  * @returns {array} profiles
34 32
  */
35 33
 const updateQueueByProfileId = async (profileId, targetId, reinsert) => {
36
-    const updateQueue = await db.patch(
37
-        `/profile/${profileId}/queue/${targetId}/delete?include_profile=true&reinsert=${reinsert}`,
38
-    )
34
+    let updateQueue
35
+    try {
36
+        updateQueue = await db.patch(
37
+            `/profile/${profileId}/queue/${targetId}/delete?include_profile=true&reinsert=${reinsert}`,
38
+        )
39
+        if (!updateQueue?.length) {
40
+            throw '[Queue Service]: Could not update match queue.'
41
+        }
42
+    } catch (err) {
43
+        console.error(err)
44
+    }
39 45
     return updateQueue
40 46
         ? updateQueue.map(profileData => {
41 47
               return new Profile({ email: 'fixme@gmail.com', ...profileData })

+ 13
- 7
frontend/src/services/survey.service.js Переглянути файл

@@ -1,4 +1,4 @@
1
-import { db } from '../utils/db'
1
+import { db } from '../utils/db.js'
2 2
 
3 3
 /**
4 4
  * Get Survey for first time profile creation from the database and
@@ -8,12 +8,18 @@ import { db } from '../utils/db'
8 8
  * @returns {array} instantiated Profile objects (see: /entites/profile)
9 9
  */
10 10
 const fetchQuestions = async () => {
11
-    const questions = await db.get(`/survey/questions`)
12
-    // Add responses to match the format from the survery factory
13
-    return questions.map(q => {
14
-        q.responses = !q.responses ? [] : q.responses
15
-        return q
16
-    })
11
+    let withResponses
12
+    try {
13
+        const questions = await db.get(`/survey/questions`)
14
+        // Add responses to match the format from the survery factory
15
+        withResponses = questions.map(q => {
16
+            q.responses = !q.responses ? [] : q.responses
17
+            return q
18
+        })
19
+    } catch (error) {
20
+        console.error(`[Survey Service]: ${error}\nquestions: ${withResponses}`)
21
+    }
22
+    return withResponses
17 23
 }
18 24
 
19 25
 const updateSurveyByProfileId = async (surveyResponses, profileId) => {

+ 2
- 2
frontend/src/services/user.service.js Переглянути файл

@@ -1,4 +1,4 @@
1
-import { db } from '../utils/db'
1
+import { db } from '../utils/db.js'
2 2
 
3 3
 /**
4 4
  * Signup a new user
@@ -8,7 +8,7 @@ const signupUser = async user => {
8 8
     const payload = {
9 9
         user_name: user.name,
10 10
         user_email: user.email,
11
-        is_poster: user.seeking == 'position' ? 0 : 1
11
+        is_poster: user.seeking == 'position' ? 0 : 1,
12 12
     }
13 13
     return await db.post(`/user/signup`, payload)
14 14
 }

+ 30
- 32
frontend/src/utils/db.js Переглянути файл

@@ -21,53 +21,51 @@ const headerTemplate = {
21 21
 class Connector {
22 22
     constructor() {
23 23
         this.apiPrefix = prefix
24
+        this._verbs = {
25
+            get: 'GET',
26
+            post: 'POST',
27
+            patch: 'PATCH',
28
+        }
24 29
     }
25
-    async get(endpoint) {
30
+    _makeHeader({ method, payload }) {
26 31
         const header = { ...headerTemplate }
27
-        header.method = 'GET'
28
-        try {
29
-            // console.log(`${remote}${endpoint}`)
30
-            let res = await fetch(`${remote}${endpoint}`, header)
31
-            if (!res.ok) {
32
-                throw Error(res.statusText)
33
-            }
34
-            const jsonRes = await res.json()
35
-            return jsonRes.data
36
-        } catch (error) {
37
-            console.error(error)
32
+        header.method = method
33
+        if (payload) {
34
+            header.body = JSON.stringify(payload)
38 35
         }
36
+        return header
39 37
     }
40
-    async post(endpoint, payload = {}) {
41
-        const header = { ...headerTemplate }
42
-        header.method = 'POST'
43
-        header.body = JSON.stringify(payload)
38
+    async _tryFetch({ endpoint, header }) {
44 39
         try {
45
-            let res = await fetch(`${remote}${endpoint}`, header)
40
+            const res = await fetch(`${remote}${endpoint}`, header)
46 41
             if (!res.ok) {
47 42
                 throw Error(res.statusText)
48 43
             }
49 44
             const jsonRes = await res.json()
50 45
             return jsonRes.data
51 46
         } catch (error) {
52
-            console.error(error)
47
+            console.error(`[API Util]: ${error}\nroute:`, endpoint)
53 48
         }
54 49
     }
50
+    async get(endpoint) {
51
+        return await this._tryFetch({
52
+            endpoint,
53
+            header: this._makeHeader({ method: this._verbs.get }),
54
+        })
55
+    }
56
+    async post(endpoint, payload = {}) {
57
+        return await this._tryFetch({
58
+            endpoint,
59
+            header: this._makeHeader({ method: this._verbs.post, payload }),
60
+        })
61
+    }
55 62
     async patch(endpoint, payload = {}) {
56
-        const header = { ...headerTemplate }
57
-        header.method = 'PATCH'
58
-        header.body = JSON.stringify(payload)
59
-        try {
60
-            let res = await fetch(`${remote}${endpoint}`, header)
61
-            if (!res.ok) {
62
-                throw Error(res.statusText)
63
-            }
64
-            const jsonRes = await res.json()
65
-            return jsonRes.data
66
-        } catch (error) {
67
-            console.error(error)
68
-        }
63
+        return await this._tryFetch({
64
+            endpoint,
65
+            header: this._makeHeader({ method: this._verbs.patch, payload }),
66
+        })
69 67
     }
70
-    
68
+
71 69
     /** !: DEV ONLY */
72 70
     // async removeAll() { }
73 71
 }

+ 64
- 35
frontend/src/utils/index.js Переглянути файл

@@ -1,18 +1,39 @@
1 1
 import Joi from 'joi'
2
+import { SurveyFactory } from './survey.js'
3
+import { possible } from './lang.js'
4
+import { pidMixin, profileMixin } from './mixins.js'
2 5
 
3
-import { Connector } from './db'
4
-import { SurveyFactory } from './survey'
5
-import { possible } from './lang'
6
-import { pidMixin, cardMixin } from './mixins'
7
-
8
-import { possibleZipcodes } from '../../../backend/db/data-generator/config.json'
9
-
10
-const api = new Connector('kittens')
6
+// This will NOT work until ES2022 gets assert in browsers
7
+// import config from '../../../backend/db/data-generator/config.json' assert { type: 'json' }
8
+// TODO: Remove this when assert works in browsers
9
+const possibleZipcodes = [
10
+    '90012',
11
+    '90040',
12
+    '90058',
13
+    '90064',
14
+    '90065',
15
+    '90240',
16
+    '90274',
17
+    '90278',
18
+    '90280',
19
+    '90290',
20
+    '90291',
21
+    '90292',
22
+    '90293',
23
+    '90840',
24
+    '91001',
25
+    '91011',
26
+    '91030',
27
+    '91201',
28
+    '91399',
29
+    '91401',
30
+    '97075',
31
+]
11 32
 
12 33
 const validatorMapping = {
13 34
     'input-string': Joi.string(),
14 35
     'tag-cloud': Joi.string(),
15
-    'checklist': Joi.string(),
36
+    checklist: Joi.string(),
16 37
     'input-slide': Joi.string(),
17 38
 }
18 39
 
@@ -20,11 +41,9 @@ const makeKebob = input => {
20 41
     return input.toLowerCase().split(' ').join('-')
21 42
 }
22 43
 
23
-const surveyFactory = new SurveyFactory(possible['usa'])
24
-
25
-
26
-const mixins = { pidMixin, cardMixin }
44
+const surveyFactory = new SurveyFactory()
27 45
 
46
+const mixins = { pidMixin, profileMixin }
28 47
 
29 48
 const randomNumber = max => {
30 49
     return Math.floor(Math.random() * max) < 1
@@ -75,32 +94,42 @@ const randomMedia = () => {
75 94
 
76 95
 const randomSurveyResponses = count => {
77 96
     const surveyResponses = [
78
-        { id: null, "idOrPrompt": "email", "val": `${randomEmail()}` },
79
-        { id: null, "idOrPrompt": "name", "val": `john test-${count}` },
80
-        { id: 99, "idOrPrompt": 15, "val": randomValFrom(possible.usa.pronouns) },
81
-        { id: null, "idOrPrompt": "seeking", "val": Math.random() > 0.2 ? possible.usa.seeking[0] : possible.usa.seeking[1] },
82
-        { id: 99, "idOrPrompt": 13, "val": randomValFrom(possible.usa.urgency) },
83
-        { id: null, "idOrPrompt": "experience", "val": randomValFrom(possible.usa.experience) },
84
-        { id: 99, "idOrPrompt": 14, "val": "swe" },
85
-        { id: 99, "idOrPrompt": 10, "val": randomValFrom(possible.usa.duration) },
86
-        { id: 99, "idOrPrompt": 9, "val": randomValFrom(possible.usa.language) },
87
-        { id: 99, "idOrPrompt": 11, "val": randomValFrom(possible.usa.presence) },
88
-        { id: 99, "idOrPrompt": 7, "val": `${randomValFrom(possibleZipcodes)}` },
89
-        { id: 99, "idOrPrompt": 16, "val": `${randomNumber(55)}` },
90
-        { id: 99, "idOrPrompt": 12, "val": "this is a test of the survey signup" },
91
-        { id: 99, "idOrPrompt": 8, "val": randomMedia() },
92
-        { id: 99, "idOrPrompt": 1, "val": `${randomNumber(3) - randomNumber(3)}` },
93
-        { id: 99, "idOrPrompt": 2, "val": `${randomNumber(3) - randomNumber(3)}` },
94
-        { id: 99, "idOrPrompt": 3, "val": `${randomNumber(3) - randomNumber(3)}` },
95
-        { id: 99, "idOrPrompt": 4, "val": `${randomNumber(3) - randomNumber(3)}` },
96
-        { id: 99, "idOrPrompt": 5, "val": `${randomNumber(3) - randomNumber(3)}` },
97
-        { id: 99, "idOrPrompt": 6, "val": `${randomNumber(3) - randomNumber(3)}` }
97
+        { id: null, idOrPrompt: 'email', val: `${randomEmail()}` },
98
+        { id: null, idOrPrompt: 'name', val: `john test-${count}` },
99
+        { id: 99, idOrPrompt: 15, val: randomValFrom(possible.usa.pronouns) },
100
+        {
101
+            id: null,
102
+            idOrPrompt: 'seeking',
103
+            val:
104
+                Math.random() > 0.2
105
+                    ? possible.usa.seeking[0]
106
+                    : possible.usa.seeking[1],
107
+        },
108
+        { id: 99, idOrPrompt: 13, val: randomValFrom(possible.usa.urgency) },
109
+        {
110
+            id: null,
111
+            idOrPrompt: 'experience',
112
+            val: randomValFrom(possible.usa.experience),
113
+        },
114
+        { id: 99, idOrPrompt: 14, val: 'swe' },
115
+        { id: 99, idOrPrompt: 10, val: randomValFrom(possible.usa.duration) },
116
+        { id: 99, idOrPrompt: 9, val: randomValFrom(possible.usa.language) },
117
+        { id: 99, idOrPrompt: 11, val: randomValFrom(possible.usa.presence) },
118
+        { id: 99, idOrPrompt: 7, val: `${randomValFrom(possibleZipcodes)}` },
119
+        { id: 99, idOrPrompt: 16, val: `${randomNumber(55)}` },
120
+        { id: 99, idOrPrompt: 12, val: 'this is a test of the survey signup' },
121
+        { id: 99, idOrPrompt: 8, val: randomMedia() },
122
+        { id: 99, idOrPrompt: 1, val: `${randomNumber(3) - randomNumber(3)}` },
123
+        { id: 99, idOrPrompt: 2, val: `${randomNumber(3) - randomNumber(3)}` },
124
+        { id: 99, idOrPrompt: 3, val: `${randomNumber(3) - randomNumber(3)}` },
125
+        { id: 99, idOrPrompt: 4, val: `${randomNumber(3) - randomNumber(3)}` },
126
+        { id: 99, idOrPrompt: 5, val: `${randomNumber(3) - randomNumber(3)}` },
127
+        { id: 99, idOrPrompt: 6, val: `${randomNumber(3) - randomNumber(3)}` },
98 128
     ]
99 129
     return surveyResponses
100 130
 }
101 131
 
102 132
 export {
103
-    api,
104 133
     validatorMapping,
105 134
     surveyFactory,
106 135
     makeKebob,
@@ -110,5 +139,5 @@ export {
110 139
     randomValFrom,
111 140
     randomMedia,
112 141
     randomName,
113
-    randomEmail
142
+    randomEmail,
114 143
 }

+ 23
- 55
frontend/src/utils/lang.js Переглянути файл

@@ -3,21 +3,24 @@ const DELIMITER = '_'
3 3
 // TODO: Combine these two
4 4
 const allSteps = {
5 5
     usa: {
6
-        email: 'email',
6
+        splash: 'splash',
7 7
         name: 'name',
8
-        pronouns: 'pronouns',
9
-        seeking: 'seeking',
10
-        urgency: 'urgency',
11
-        experience: 'experience',
12
-        roles: 'role',
13
-        duration: 'duration',
14
-        presence: 'presence',
15
-        language: 'language',
8
+        email: 'email',
9
+        password: 'password',
10
+        // pronouns: 'pronouns',
11
+        // seeking: 'seeking',
12
+        // urgency: 'urgency',
13
+        // experience: 'experience',
14
+        // roles: 'role',
15
+        // duration: 'duration',
16
+        // presence: 'presence',
17
+        // language: 'language',
16 18
         zipcode: 'zipcode',
17
-        distance: 'distance',
19
+        // distance: 'distance',
18 20
         blurb: 'blurb',
19 21
         image: 'image',
20
-    }
22
+        aspects: 'aspects'
23
+    },
21 24
 }
22 25
 
23 26
 const aspectResponses = {
@@ -29,7 +32,7 @@ const aspectResponses = {
29 32
         mostly: 'mostly',
30 33
         often: 'often',
31 34
         everytime: 'everytime',
32
-    }
35
+    },
33 36
 }
34 37
 
35 38
 const possible = {}
@@ -37,17 +40,7 @@ possible.usa = {
37 40
     email: [],
38 41
     name: [],
39 42
     seeking: ['position', 'candidate'],
40
-    language: [
41
-        'javascript',
42
-        'python',
43
-        'c#',
44
-        'c++',
45
-        'go',
46
-        'java',
47
-        'ruby',
48
-        'html',
49
-        'css',
50
-    ],
43
+    language: ['english', 'spanish'],
51 44
     // key 13
52 45
     urgency: [
53 46
         'actively_looking',
@@ -55,27 +48,11 @@ possible.usa = {
55 48
         'casually_browsing',
56 49
     ],
57 50
     // key 11
58
-    presence: [
59
-        'remote',
60
-        'in_person',
61
-        'hybrid',
62
-        'flexible'
63
-    ],
51
+    presence: ['remote', 'in_person', 'hybrid', 'flexible'],
64 52
     // key 10
65
-    duration: [
66
-        'full-time',
67
-        'part-time',
68
-        'contract',
69
-        'flexible',
70
-    ],
53
+    duration: ['full-time', 'part-time', 'contract', 'flexible'],
71 54
     // Experience and roles concat, key: 14
72
-    experience: [
73
-        'associate',
74
-        'junior',
75
-        'mid-level',
76
-        'senior',
77
-        'staff',
78
-    ],
55
+    experience: ['associate', 'junior', 'mid-level', 'senior', 'staff'],
79 56
     roles: {
80 57
         type: [
81 58
             'back-end',
@@ -96,21 +73,12 @@ possible.usa = {
96 73
             'manager',
97 74
             'technician',
98 75
         ],
99
-        candidate: [
100
-            'hiring_manager',
101
-            'recruiter',
102
-        ]
76
+        candidate: ['hiring_manager', 'recruiter'],
103 77
     },
104
-    pronouns: [
105
-        'she/her',
106
-        'she/they',
107
-        'he/him',
108
-        'he/they',
109
-        'they/them',
110
-    ],
78
+    pronouns: ['she/her', 'she/they', 'he/him', 'he/they', 'they/them'],
111 79
     image: [],
112 80
     zipcode: [],
113
-    blurb: []
81
+    blurb: [],
114 82
 }
115 83
 
116
-export { allSteps, aspectResponses, possible }
84
+export { allSteps, aspectResponses, possible, DELIMITER }

+ 12
- 19
frontend/src/utils/mixins.js Переглянути файл

@@ -1,3 +1,5 @@
1
+import { currentProfile } from '../services'
2
+
1 3
 const pidMixin = {
2 4
     props: {
3 5
         pid: {
@@ -8,26 +10,17 @@ const pidMixin = {
8 10
     },
9 11
 }
10 12
 
11
-const cardMixin = {
12
-    data: () => ({
13
-        cards: [],
14
-        matches: [],
15
-        loading: true,
16
-    }),
17
-    watch: {
18
-        /** Fetch the queue if pid changes */
19
-        pid() {
20
-            this.getCards()
13
+const profileMixin = {
14
+    computed: {
15
+        profile() {
16
+            return currentProfile
21 17
         },
22
-    },
23
-    async created() {
24
-        await this.getCards()
25
-    },
26
-    methods: {
27
-        _reformat(data, mapCb) {
28
-            return data.map(mapCb)
18
+        isLoading() {
19
+            return currentProfile.isLoading
20
+        },
21
+        isLoggedIn() {
22
+            return currentProfile.isLoggedIn
29 23
         },
30 24
     },
31 25
 }
32
-
33
-export { pidMixin, cardMixin }
26
+export { pidMixin, profileMixin }

+ 68
- 18
frontend/src/utils/survey.js Переглянути файл

@@ -1,45 +1,95 @@
1
-import { Survey } from '../entities'
2
-import { fetchQuestions } from '../services'
1
+import { Survey } from '../entities/index.js'
2
+import { fetchQuestions } from '../services/index.js'
3
+import { possible } from './lang.js'
4
+
5
+const promptToComponent = {
6
+    splash: 'Splash',
7
+    name: 'FormInput',
8
+    email: 'FormInput',
9
+    password: 'FormInput',
10
+    zipcode: 'FormInput',
11
+    // seeking: 'FormDropdown',
12
+    // urgency: 'FormDropdown',
13
+    // presence: 'FormDropdown',
14
+    // duration: 'FormDropdown',
15
+    // experience: 'FormTags',
16
+    // pronouns: 'FormDropdown',
17
+    // language: 'FormDropdown',
18
+    image: 'FormInput',
19
+    // distance: 'FormInput',
20
+    blurb: 'FormInput',
21
+    aspects: 'Aspects'
22
+}
23
+/**
24
+ * Make a step from match or step information
25
+ * @param {object} match
26
+ * @param {object} step
27
+ * @returns something like a response_key with possible responses
28
+ */
29
+const formatStep = (match, step) => {
30
+    const responsesByCategory = possible['usa']
31
+    const responseKey = {
32
+        response_key_id: match ? match.response_key_id : null,
33
+        response_key_category: match ? match.response_key_category : 'profile',
34
+        response_key_prompt: match ? match.response_key_prompt : step,
35
+        response_key_description: match ? match.response_key_description : null,
36
+    }
37
+    return {
38
+        ...responseKey,
39
+        responses: responsesByCategory[step] ? responsesByCategory[step] : [],
40
+    }
41
+}
42
+const associateWithComponent = responseKeyLike => {
43
+    let component = promptToComponent[responseKeyLike.response_key_prompt]
44
+    return { ...responseKeyLike, component }
45
+}
46
+
47
+const hasMatch = (step, inArray) => {
48
+    return inArray.find(q => q.response_key_prompt == step)
49
+}
3 50
 
4 51
 class SurveyFactory {
5
-    constructor(responses) {
6
-        this.responsesByCategory = responses
52
+    constructor() {
7 53
         this.questionsFromDb = []
8 54
     }
9 55
     _setSteps(langFile) {
10
-        const stepsToProcess = [...Object.values(langFile) ]
56
+        const stepsToProcess = [...Object.values(langFile)]
11 57
         const seenIds = []
12 58
         const stepsInCommon = stepsToProcess.map(step => {
13 59
             // Match question to step
14
-            const match = this.questionsFromDb.filter(q => q.response_key_prompt == step)[0]
15
-            if(match) { seenIds.push(match.response_key_id) }
16
-            return {
17
-                response_key_id: match ? match.response_key_id: null,
18
-                response_key_category: match ? match.response_key_category: 'profile',
19
-                response_key_prompt: match ? match.response_key_prompt: step,
20
-                response_key_description: match ? match.response_key_description: null,
21
-                responses: this.responsesByCategory[step] ? this.responsesByCategory[step] : [] 
60
+            const match = hasMatch(step, this.questionsFromDb)
61
+            if (match) {
62
+                seenIds.push(match.response_key_id)
22 63
             }
64
+            const responseKeyLike = formatStep(match, step)
65
+            const withComponent = associateWithComponent(responseKeyLike)
66
+            console.log('withComponent :>> ', withComponent)
67
+            return withComponent
23 68
         })
24
-        const unseen = this.questionsFromDb.filter(q => !seenIds.includes(q.response_key_id))
69
+        // temporary extra condition in filter 
70
+        let unseen = this.questionsFromDb.filter(
71
+            q => !seenIds.includes(q.response_key_id) && [1,2,3,4,5,6].includes(q.response_key_id),
72
+        )
25 73
         return [...stepsInCommon, ...unseen]
26 74
     }
27 75
     async getQuestions() {
28 76
         try {
29 77
             this.questionsFromDb = await fetchQuestions()
30 78
             return this.questionsFromDb
31
-        } catch(err) {
79
+        } catch (err) {
32 80
             console.error(err)
33 81
         }
34 82
     }
35 83
     async createSurvey(langFile, roleTree) {
36
-        if(!this.questionsFromDb.length) {
84
+        if (!this.questionsFromDb.length) {
37 85
             const res = await this.getQuestions()
38
-            console.warn(`Attempted to create a survey before getting questions: retrieved ${res.length} questions`)
86
+            console.warn(
87
+                `Attempted to create a survey before getting questions: retrieved ${res.length} questions`,
88
+            )
39 89
         }
40 90
         const steps = this._setSteps(langFile)
41 91
         return new Survey(steps, roleTree)
42 92
     }
43 93
 }
44 94
 
45
-export { SurveyFactory }
95
+export { SurveyFactory }

+ 55
- 27
frontend/src/views/ChatView.vue Переглянути файл

@@ -1,27 +1,42 @@
1 1
 <template lang="pug">
2 2
 main.view--chat
3
-    header.mb6(v-if='profile')
4
-        h3 chatting with: {{ target.profile_id }}
5
-        p subscriptions: {{ profile.chatter.subscriptions }}
3
+    header.mb6(v-if='profile && grouping')
4
+        h3 chatting with:
5
+        p {{ target.profile_id }} | {{ grouping.profile.user_name }} | {{ grouping.profile.user_email }}
6 6
 
7
-    article(v-if='profile.isLoggedIn && target')
8
-        ul#messages.w-flex.column
9
-            template(v-for='chatmessage in messages')
10
-                li(
11
-                    :class='[{ me: chatmessage.publisher == profile.id.value }, chatmessage.publisher]'
12
-                    v-if='chatmessage.message.description'
13
-                )
14
-                    .message-wrapper.pa4
15
-                        p.pb1
16
-                            span(
17
-                                v-if='chatmessage.publisher == profile.id.value'
18
-                            ) you
19
-                            span(v-else) {{ chatmessage.publisher }}
20
-                            span @{{ new Date(parseInt(chatmessage.timetoken) / 10000).toLocaleTimeString('en-US') }}
21
-                        p {{ chatmessage.message.description }}
7
+        h3 logged in as:
8
+        p {{ profile.id }} | {{ profile._profile.user_name }} | {{ profile._profile.user_email }}
9
+        //- p subscriptions: {{ profile.chatter.subscriptions }}
10
+        hr
22 11
 
23
-    p(v-else-if='profile.isLoggedIn && !target') No match found between profile {{ $route.params.pid }} and {{ profile.id }}...
24
-    w-spinner(bounce v-else)
12
+        .w-flex.column(v-if='!grouping._loading')
13
+            .w-flex.row
14
+                button(@click='reveal(7)') reveal my name
15
+                button(@click='reveal(8)') reveal my email
16
+            p you revealed: 
17
+                span(v-if="grouping.revealed[profile.id]") {{ grouping.revealed }}
18
+            p they revealed:
19
+                span(v-if="grouping.revealed[grouping.profile.profile_id]") {{ grouping.revealed[grouping.profile.profile_id] }}
20
+
21
+    article
22
+        template(v-if='isLoading')
23
+            w-spinner(bounce)
24
+
25
+        template(v-if='!isLoading && target')
26
+            ul#messages.w-flex.column
27
+                template(v-for='chatmessage in messages')
28
+                    li(
29
+                        :class='[{ me: chatmessage.publisher == profile.id.value }, chatmessage.publisher]'
30
+                        v-if='chatmessage.message && chatmessage.message.description'
31
+                    )
32
+                        ChatBubble(
33
+                            :message='chatmessage.message.description'
34
+                            :publisher-id='chatmessage.publisher'
35
+                            :time='chatmessage.timetoken'
36
+                        )
37
+
38
+        template(v-else-if='!isLoading && !target')
39
+            p No match found between profile {{ $route.params.pid }} and {{ profile.id }}...
25 40
 
26 41
     footer.w-flex.row
27 42
         w-input(@keyup.enter='sendMessage' outline v-model='toSend')
@@ -31,31 +46,44 @@ main.view--chat
31 46
     MainNav
32 47
 </template>
33 48
 
34
-<script>
49
+<script lang="js">
50
+import ChatBubble from '../components/ChatBubble.vue'
35 51
 import { currentProfile } from '../services'
52
+import { mixins } from '../utils'
36 53
 
37 54
 export default {
38 55
     name: 'ProfileView',
56
+    components: { ChatBubble },
57
+    mixins: [mixins.profileMixin],
39 58
     data: () => ({
40 59
         target: null,
41 60
         toSend: '',
42 61
         messages: [],
43
-        openDrawer: null,
62
+        grouping: null,
44 63
     }),
45
-    computed: {
46
-        profile: () => currentProfile,
47
-    },
48 64
     watch: {
49
-        profile() {
65
+        async profile() {
50 66
             this.loadTargetProfile()
67
+            currentProfile._loading.value = true
68
+            this.messages = await currentProfile.chatter.getHistory(this.grouping.grouping_name)
51 69
             currentProfile.chatter.setOnMessage(this._onMessage)
70
+            currentProfile._loading.value = false
52 71
         },
53 72
     },
54
-    created() {
73
+    async created() {
55 74
         this.loadTargetProfile()
75
+        currentProfile._loading.value = true
76
+        this.grouping = this.getGrouping()
77
+        this.messages = await currentProfile.chatter.getHistory(this.grouping.grouping_name)
78
+        console.log('this.messages :>> ', this.messages)
56 79
         currentProfile.chatter.setOnMessage(this._onMessage)
80
+        currentProfile._loading.value = false
57 81
     },
58 82
     methods: {
83
+        async reveal(tagId) {
84
+            const grouping = this.getGrouping()
85
+            await grouping.reveal(currentProfile.id.value, tagId)
86
+        },
59 87
         /**
60 88
          * Pubnub message callback fires when message event
61 89
          * is detected. We define is HERE because we need the

+ 28
- 83
frontend/src/views/HomeView.vue Переглянути файл

@@ -1,54 +1,41 @@
1 1
 <template lang="pug">
2 2
 main.view--home
3
-    
4
-    article(v-if='cards.length && !loading')
5
-        ProfileCardList(:pid='pid' :cards='cards' @reload='getCards')
3
+    article.w-flex.column.align-center
4
+        template(v-if='isLoading')
5
+            w-spinner(bounce)
6 6
 
7
-    p(v-else-if='cards.length === 0') No profiles in match_queue.
8
-    w-spinner(v-else bounce)
9
-
10
-    footer
11
-        w-form
12
-            w-input.mb2(
13
-                inner-icon-left='mdi mdi-account'
14
-                label='Full Name'
15
-                label-position='inside'
16
-                outline
17
-            )
18
-            w-input.mb2(
19
-                inner-icon-left='mdi mdi-mail'
20
-                label='E-mail'
21
-                label-position='inside'
22
-                outline
23
-            )
24
-            w-input.mb2(
25
-                inner-icon-left='mdi mdi-lock'
26
-                label='Password'
27
-                label-position='inside'
28
-                outline
29
-                type='password'
30
-            )
7
+        template(v-else-if='!isLoading && cards.length > 0')
8
+            ProfileCardList(:cards='cards')
31 9
 
10
+        template(v-else-if='cards.length === 0')
11
+            p No profiles in match_queue.
32 12
     MainNav
33 13
 </template>
34 14
 
35 15
 <script>
36 16
 import 'wave-ui/dist/wave-ui.css'
37 17
 import ProfileCardList from '../components/ProfileCardList.vue'
38
-import TagList from '../components/TagList.vue'
39 18
 import AspectBar from '../components/AspectBar.vue'
40 19
 import SummaryBar from '../components/SummaryBar.vue'
41 20
 import PairingButton from '../components/PairingButton.vue'
42 21
 
43 22
 import { Card } from '../entities'
44 23
 
45
-import {
46
-    fetchQueueByProfileId,
47
-    fetchMembershipsByProfileId,
48
-    currentProfile,
49
-} from '../services'
24
+import { currentProfile } from '../services'
50 25
 import { mixins } from '../utils'
51 26
 
27
+const notificationOpts = {
28
+    message: null,
29
+    timeout: 6000,
30
+    bgColor: 'white',
31
+    color: 'success',
32
+    dismiss: false,
33
+    shadow: true,
34
+    round: true,
35
+    sm: true,
36
+    icon: 'wi-star',
37
+}
38
+
52 39
 /** Callback used to format incoming into card */
53 40
 const convertToCard = profile => {
54 41
     if (profile.type !== 'profile') {
@@ -64,67 +51,25 @@ const convertToCard = profile => {
64 51
     })
65 52
 }
66 53
 
67
-const converGroupingToCard = grouping => {
68
-    if (grouping.type !== 'grouping') {
69
-        console.error(`Cannot convert ${grouping} to Card. Invalid entity.`)
70
-    }
71
-    if (!grouping.profile.isValid()) {
72
-        console.warn(`Profile in ${grouping} is not a valid profile.`)
73
-    }
74
-    return new Card({
75
-        pid: grouping.profile.profile_id,
76
-        name: grouping.profile.user_name,
77
-        avatar: grouping.profile.profile_media[0],
78
-    })
79
-}
80
-
81 54
 export default {
82 55
     name: 'HomeView',
83 56
     components: {
84 57
         ProfileCardList,
85 58
         AspectBar,
86
-        TagList,
87 59
         SummaryBar,
88 60
         PairingButton,
89 61
     },
90
-    mixins: [mixins.pidMixin, mixins.cardMixin],
91
-    methods: {
92
-        /** Gets called from cardMixin */
93
-        async getCards() {
94
-            this.loading = true
95
-            try {
96
-                const queueList = await fetchQueueByProfileId(this.pid)
97
-                this.cards = this._reformat(queueList, convertToCard)
98
-                const matchList = await fetchMembershipsByProfileId(this.pid)
99
-                this.matches = this._reformat(matchList, converGroupingToCard)
100
-            } catch (err) {
101
-                console.error(err)
102
-            }
103
-            this.loading = false
62
+    mixins: [mixins.profileMixin],
63
+    computed: {
64
+        cards() {
65
+            return currentProfile.queue.map(qProfile => convertToCard(qProfile))
104 66
         },
67
+    },
68
+    methods: {
105 69
         // this can be placed in utils/notification.js
106
-
107 70
         notify(payload) {
108
-            this.$waveui.notify({
109
-                message: payload,
110
-                timeout: 6000,
111
-                bgColor: 'white',
112
-                color: 'success',
113
-                dismiss: false,
114
-                shadow: true,
115
-                round: true,
116
-                sm: true,
117
-                icon: 'wi-star',
118
-            })
119
-        },
120
-        //  a way to send a message to a user for development purposes and testing
121
-        async chat() {
122
-            const chatter = currentProfile.chatter
123
-            const res = await chatter.publish(chatter.subscriptions[0], {
124
-                title: 'New Message',
125
-                description: 'This is a new message',
126
-            })
127
-            this.notify(res)
71
+            notificationOpts.message = payload
72
+            this.$waveui.notify(notificationOpts)
128 73
         },
129 74
     },
130 75
 }

+ 7
- 19
frontend/src/views/MessagesView.vue Переглянути файл

@@ -1,36 +1,24 @@
1 1
 <template lang="pug">
2 2
 main.view--messages
3
-    article(v-if="!loading")
4
-        PairsList(:pairs="inboxes")
3
+    article
4
+        template(v-if='!loading')
5
+            w-spinner(bounce)
5 6
 
6
-    w-spinner(v-else bounce)
7
+        template(v-else)
8
+            PairsList(:pairs='inboxes')
7 9
     MainNav
8 10
 </template>
9 11
 
10 12
 <script>
11 13
 import PairsList from '../components/PairsList.vue'
12
-
13
-import { fetchMembershipsByProfileId } from '../services'
14 14
 import { mixins } from '../utils'
15 15
 
16 16
 export default {
17 17
     name: 'MessagesView',
18 18
     components: { PairsList },
19
-    mixins: [mixins.pidMixin, mixins.cardMixin],
19
+    mixins: [mixins.pidMixin, mixins.profileMixin],
20 20
     data: () => ({
21 21
         inboxes: ['x'],
22 22
     }),
23
-    methods: {
24
-        /** Gets called from cardMixin */
25
-        async getCards() {
26
-            this.loading = true
27
-            try {
28
-                this.inboxes = await fetchMembershipsByProfileId(this.pid)
29
-            } catch (err) {
30
-                console.error(err)
31
-            }
32
-            this.loading = false
33
-        },
34
-    },
35 23
 }
36
-</script>
24
+</script>

+ 139
- 52
frontend/src/views/OnboardingView.vue Переглянути файл

@@ -1,70 +1,157 @@
1 1
 <template lang="pug">
2 2
 main.view--onboarding
3
-    article
4
-        form(@submit.prevent="onSubmit").questionnaire
5
-            QuestionResponse(v-for="question in questions" :question="question" @updated="onUpdate")
6
-            w-button.ma1.grow(type="submit" bg-color="success")
7
-                w-icon.mr1 wi-check
8
-                | SUBMIT ANSWERS
9
-    MainNav
3
+    article(
4
+        style='display: flex; flex-direction: column; align-items: center'
5
+        v-if='survey'
6
+    )
7
+        .step(v-for='(step, i) in survey.steps')
8
+            component(
9
+                :aspect-questions='step.component == "Aspects" ? survey.aspectQuestions : null'
10
+                :is='step.component'
11
+                :question='step'
12
+                @handle-submit='onSubmit'
13
+                @update-answers='updateAnswers'
14
+                v-if='step && currentStep == i'
15
+            )
10 16
 </template>
11 17
 
12 18
 <script>
13 19
 import { surveyFactory } from '@/utils'
14
-import { allSteps, possible } from '@/utils/lang'
15
-
16
-import QuestionResponse from '../components/QuestionResponse.vue'
17
-const SCORED = [1, 2, 3, 4, 5, 6]
18
-const _isScored = id => SCORED.includes(id)
19
-
20
-const _makeCategoryFriendly = responseCategory => {
21
-    const labels = responseCategory.split('_vs_')
22
-    labels.forEach((a, i) => {
23
-        if (a.indexOf('_') == -1) return
24
-        labels[i] = a.split('_').join(' ')
25
-    })
26
-    return labels
27
-}
28
-
29
-const _formatSteps = steps => {
30
-    return steps
31
-        .map(q => {
32
-            if (!_isScored(q.response_key_id)) return null
33
-            return {
34
-                id: q.response_key_id,
35
-                question: q.response_key_prompt,
36
-                labels: _makeCategoryFriendly(q.response_key_category),
37
-            }
38
-        })
39
-        .filter(step => step != null)
40
-}
20
+import { allSteps } from '@/utils/lang'
21
+import stepViews from '@/components/onboarding'
41 22
 
23
+// import savesurveybyprfileid - call it on submit
24
+// paginate to save every steps answers
42 25
 export default {
43 26
     name: 'OnboardingView',
44
-    components: { QuestionResponse },
45
-    data: () => {
46
-        return {
47
-            validSurvey: null,
48
-            questions: [],
49
-            answered: {},
50
-        }
27
+    components: {
28
+        ...stepViews,
51 29
     },
30
+    data: () => ({
31
+        answered: {},
32
+        aspectQuestions: [],
33
+        currentStep: 0,
34
+        survey: null,
35
+    }),
52 36
     async created() {
53
-        const survey = await surveyFactory.createSurvey(
54
-            allSteps['usa'],
55
-            possible['usa']['roles'],
56
-        )
57
-        this.questions = _formatSteps(survey.steps)
37
+        this.survey = await surveyFactory.createSurvey(allSteps['usa'])
58 38
     },
59 39
     methods: {
60
-        onUpdate(payload) {
61
-            this.answered[payload.id] = payload
62
-        },
63 40
         onSubmit() {
64
-            Object.values(this.answered).forEach(ans =>
65
-                console.log(ans.question, ans.answer),
66
-            )
41
+            console.log(JSON.stringify(this.answered))
42
+        },
43
+        goToStep(num) {
44
+            this.currentStep = num
45
+        },
46
+        updateAnswers(payload) {
47
+            // null payload is passed on splash page
48
+            if (payload) {
49
+                const k = payload.question.response_key_prompt
50
+                this.answered[k] = payload.answer
51
+                console.log(`${k}:`, this.answered[k])
52
+                console.log(`Updated answers: ${JSON.stringify(this.answered)}`)
53
+                if (k === 'aspects') return
54
+            }
55
+            this.goToStep(this.currentStep + 1)
67 56
         },
68 57
     },
69 58
 }
70 59
 </script>
60
+
61
+<style lang="sass">
62
+.view--onboarding
63
+    width: 100%
64
+    max-width: 428px
65
+    background-color: #fff
66
+    color: #1F2024
67
+    font-family: Century Gothic,CenturyGothic,AppleGothic,sans-serif
68
+    margin: 0 auto
69
+
70
+    article
71
+        height: 100vh
72
+
73
+    .w-button
74
+            display: flex
75
+            width: 315px
76
+            height: 60px
77
+            border-radius: 6
78
+            background-color: #D5D5D5
79
+            color: #1F2024
80
+            margin: 11px auto
81
+            font-weight: bold
82
+            font-size: 16px
83
+            text-transform: uppercase
84
+            &.next-btn
85
+                border-radius: 6px
86
+                background-color: #5BA626
87
+                color: #1F2024
88
+                height: 50px
89
+                width: 315px
90
+                font-size: 18px
91
+                padding: 7px
92
+
93
+    .w-card
94
+        background-color: #1F2024
95
+        justify-content: center
96
+        align-items: center
97
+        width: 100%
98
+
99
+
100
+        h3
101
+            text-transform: uppercase
102
+            text-align: center
103
+            font-size: 28px
104
+            font-weight: bold
105
+            color: white
106
+            margin-top: 88px
107
+
108
+        p
109
+            color: white
110
+            font-size: 18px
111
+            padding: 11px
112
+            text-align: center
113
+            margin: 22px auto
114
+            font-weight: bold
115
+
116
+        input
117
+            display: flex
118
+            width: 315px
119
+            height: 60px
120
+            padding: 11px
121
+            border-radius: 6
122
+            background-color: #D5D5D5
123
+            color: #1F2024
124
+            margin: 11px auto
125
+            font-weight: bold
126
+            font-size: 16px
127
+
128
+            .w-select
129
+                padding: 11px
130
+                color: #1F2024
131
+
132
+            .search-type
133
+                color: #1F2024
134
+                height: 50px
135
+
136
+        &.question
137
+            p
138
+                font-size: 18px
139
+                text-align: left
140
+                margin: 7px auto
141
+                font-weight: normal
142
+            section
143
+                p
144
+                    margin: 0
145
+                    font-weight: bold
146
+                    text-transform: capitalize
147
+    .w-radio__input
148
+
149
+        &.primary
150
+            background-color: #FFFFFF
151
+            border: #BCC5D3 1px solid
152
+
153
+    .w-card__content
154
+        .w-button
155
+            height: 50px
156
+            background-color: #5BA626
157
+</style>

+ 41
- 34
frontend/src/views/PairsView.vue Переглянути файл

@@ -1,17 +1,19 @@
1 1
 <template lang="pug">
2 2
 main.view--pairs
3
-    article(v-if='isLoggedIn')
4
-        w-tabs(:items='tabs' fill-bar)
5
-            template(#item-title='{ item }')
6
-                span.green {{ item.title }}
7
-            //- pending tab
8
-            template(#item-content.1='{ item }')
9
-                PairsList(:pairs='pending' tab-name='pending')
10
-            //- paired tab 
11
-            template(#item-content.2='{ item }')
12
-                PairsList(:pairs='paired' tab-name='paired')
3
+    article
4
+        template(v-if='isLoading')
5
+            w-spinner(bounce)
6
+        template(v-else)
7
+            w-tabs(:items='tabs' fill-bar slider-color='yellow')
8
+                template(#item-title='{ item }')
9
+                    span {{ item.title }}
10
+                //- pending tab
11
+                template(#item-content.1='{ item }')
12
+                    PairsList(:pairs='pending' tab-name='pending')
13
+                //- paired tab 
14
+                template(#item-content.2='{ item }')
15
+                    PairsList(:pairs='paired' tab-name='paired')
13 16
 
14
-    w-spinner(bounce v-else)
15 17
     MainNav
16 18
 </template>
17 19
 
@@ -42,13 +44,11 @@ const convertGroupingToCard = grouping => {
42 44
 export default {
43 45
     name: 'PairsView',
44 46
     components: { PairsList },
47
+    mixins: [mixins.profileMixin],
45 48
     data: () => ({
46 49
         tabs: [{ title: 'Pending' }, { title: 'Paired' }],
47 50
     }),
48 51
     computed: {
49
-        isLoggedIn() {
50
-            return currentProfile.isLoggedIn
51
-        },
52 52
         pending() {
53 53
             if (!this.isLoggedIn || !currentProfile.pendingGroupings) return []
54 54
             return this._reformat(
@@ -72,24 +72,31 @@ export default {
72 72
 }
73 73
 </script>
74 74
 
75
-<style>
76
-.select--matches {
77
-    display: flex;
78
-    justify-content: space-between;
79
-    margin: 0 25px;
80
-}
81
-.select--matches > div {
82
-    width: 100%;
83
-    text-align: center;
84
-    font-size: 16px;
85
-    line-height: 40px;
86
-}
87
-.active {
88
-    border-bottom: 3px solid #f2cd5c;
89
-    color: #f2cd5c;
90
-}
91
-.idle {
92
-    color: #bcc5d3;
93
-}
75
+<style lang="sass">
76
+.view--pairs
77
+    max-width: 450px
78
+    width: 100%
79
+    margin: 0 auto
80
+    background-color: #1F2024
81
+    .w-tabs
82
+        &__bar-item
83
+            height: 50px
84
+            font-family: 'Century Gothic'
85
+            color: #FFFFFF
86
+            &.primary
87
+                color: #F2CD5C
88
+    .select--matches
89
+        display: flex
90
+        justify-content: space-between
91
+        margin: 0 25px
92
+        > div
93
+            width: 100%
94
+            text-align: center
95
+            font-size: 16px
96
+            line-height: 40px
97
+    .active
98
+        border-bottom: 3px solid #f2cd5c
99
+        color: #f2cd5c
100
+    .idle
101
+        color: #bcc5d3
94 102
 </style>
95
-s

+ 58
- 0
frontend/tests/login.service.spec.js Переглянути файл

@@ -0,0 +1,58 @@
1
+import test from 'ava'
2
+import { Login } from '../src/services/login.service.js'
3
+
4
+test('Make sure we can instantiate login service', async t => {
5
+    const currentProfile = new Login()
6
+
7
+    t.is(currentProfile.isLoggedIn, false)
8
+    t.is(currentProfile.isComplete, false)
9
+    t.is(currentProfile.hasResponses, 0)
10
+
11
+    currentProfile.id.value = 99
12
+    currentProfile._loading.value = false
13
+    t.is(currentProfile.isLoggedIn, true)
14
+})
15
+
16
+test('Make sure login.tags can be set', async t => {
17
+    const currentProfile = new Login()
18
+    const testTags = ['x', 'y', 'z']
19
+    currentProfile.setTags(testTags)
20
+    t.is(currentProfile.tags, testTags)
21
+})
22
+
23
+test('Make sure login.responses can be set', async t => {
24
+    const currentProfile = new Login()
25
+    const testResponses = ['100', '200', '300']
26
+    currentProfile.setResponses(testResponses)
27
+    t.is(currentProfile.responses, testResponses)
28
+})
29
+
30
+test('Make sure login.groupings work correctly', async t => {
31
+    const currentProfile = new Login()
32
+    const testGroupings = [
33
+        { name: 'a', is_paired: false },
34
+        { name: 'b', is_paired: false },
35
+        { name: 'c', is_paired: true },
36
+    ]
37
+    currentProfile.groupings = testGroupings
38
+    t.deepEqual(
39
+        currentProfile.pendingGroupings.map(g => g.name),
40
+        ['a', 'b'],
41
+    )
42
+    t.deepEqual(
43
+        currentProfile.pairedGroupings.map(g => g.name),
44
+        ['c'],
45
+    )
46
+})
47
+
48
+test('Make sure login.logout() works', async t => {
49
+    const currentProfile = new Login()
50
+    currentProfile.id.value = 99
51
+
52
+    currentProfile.toaster = { name: 'dummy_toaster', stop: () => {} }
53
+    currentProfile.chatter = { name: 'dummy_chatter', stop: () => {} }
54
+    t.is(currentProfile.chatter.name, 'dummy_chatter')
55
+
56
+    currentProfile.logout()
57
+    t.is(currentProfile.id.value, null)
58
+})

Завантаження…
Відмінити
Зберегти