Ver código fonte

0.0.3 release - did a bad thing merge -s ours master

tags/0.0.3
maeda 2 anos atrás
pai
commit
8cfb368ea3
94 arquivos alterados com 9749 adições e 8308 exclusões
  1. 2
    1
      .gitignore
  2. 4
    0
      backend/.env.sample
  3. 14
    8
      backend/db/data-generator/config.json
  4. 21
    22
      backend/db/data-generator/index.js
  5. 433
    39
      backend/db/data-generator/mock.js
  6. 4
    0
      backend/db/migrations/20210527174416_create_response_keys_table.js
  7. 8
    6
      backend/knexfile.js
  8. 61
    16
      backend/lib/auth/strategies/jwt.js
  9. 2
    0
      backend/lib/plugins/profile.js
  10. 9
    1
      backend/lib/plugins/user.js
  11. 49
    25
      backend/lib/routes/membership/active.js
  12. 5
    0
      backend/lib/routes/membership/join.js
  13. 37
    4
      backend/lib/routes/membership/reveal.js
  14. 1
    2
      backend/lib/routes/profile/get.js
  15. 85
    0
      backend/lib/routes/profile/insert.js
  16. 21
    17
      backend/lib/routes/profile/queue.js
  17. 6
    0
      backend/lib/routes/profile/score.js
  18. 5
    1
      backend/lib/routes/user/create-profile.js
  19. 72
    0
      backend/lib/routes/user/email.js
  20. 58
    0
      backend/lib/routes/user/getaccess.js
  21. 1
    2
      backend/lib/routes/user/list-profiles.js
  22. 5
    1
      backend/lib/routes/user/signup.js
  23. 79
    0
      backend/lib/routes/user/validatesession.js
  24. 78
    0
      backend/lib/routes/user/verifyactivesession.js
  25. 2
    0
      backend/lib/schemas/profiles.js
  26. 5
    0
      backend/lib/schemas/responses.js
  27. 48
    0
      backend/lib/services/filter.js
  28. 8
    1
      backend/lib/services/matchqueue.js
  29. 116
    101
      backend/lib/services/profile/index.js
  30. 88
    73
      backend/lib/services/profile/profiler.js
  31. 2
    7
      backend/lib/services/profile/scorer.js
  32. 0
    7
      backend/lib/services/profile/tagger.js
  33. 0
    9
      backend/lib/services/profile/zipcoder.js
  34. 140
    19
      backend/lib/services/user.js
  35. 4631
    2757
      backend/package-lock.json
  36. 4
    2
      backend/package.json
  37. 1
    0
      backend/server/index.js
  38. 0
    1
      backend/server/manifest.js
  39. BIN
      frontend/assets/fonts/icomoon.eot
  40. 42
    0
      frontend/assets/fonts/icomoon.svg
  41. BIN
      frontend/assets/fonts/icomoon.ttf
  42. BIN
      frontend/assets/fonts/icomoon.woff
  43. BIN
      frontend/assets/images/woman-1-lg.jpg
  44. BIN
      frontend/assets/images/woman-1-sm.jpg
  45. 149
    0
      frontend/assets/sass/icons.scss
  46. 13
    0
      frontend/assets/sass/main.scss
  47. 48
    0
      frontend/assets/sass/variables.scss
  48. 17
    14
      frontend/index.html
  49. 620
    4759
      frontend/package-lock.json
  50. 4
    0
      frontend/src/App.vue
  51. 35
    16
      frontend/src/components/MainNav.vue
  52. 0
    3
      frontend/src/components/NamePlate.vue
  53. 32
    14
      frontend/src/components/PairsList.vue
  54. 20
    6
      frontend/src/components/ProfileCard.vue
  55. 30
    2
      frontend/src/components/ProfileCardList.vue
  56. 1
    1
      frontend/src/components/SideBar.vue
  57. 127
    112
      frontend/src/components/SummaryBar.vue
  58. 3
    3
      frontend/src/components/TopNav.vue
  59. 2
    2
      frontend/src/components/onboarding/AccountType.vue
  60. 0
    48
      frontend/src/components/onboarding/Aspects.vue
  61. 107
    0
      frontend/src/components/onboarding/Auth.vue
  62. 13
    7
      frontend/src/components/onboarding/FormDropdown.vue
  63. 62
    11
      frontend/src/components/onboarding/FormInput.vue
  64. 37
    13
      frontend/src/components/onboarding/QuestionResponse.vue
  65. 12
    2
      frontend/src/components/onboarding/Splash.vue
  66. 4
    2
      frontend/src/components/onboarding/index.js
  67. 26
    1
      frontend/src/entities/card/card.js
  68. 2
    0
      frontend/src/entities/profile/profile.schema.js
  69. 3
    1
      frontend/src/entities/response/response.schema.js
  70. 39
    0
      frontend/src/entities/survey/survey.answer.validator.js
  71. 28
    2
      frontend/src/entities/survey/survey.js
  72. 1486
    0
      frontend/src/entities/survey/tlds-alpha-by-domain.js
  73. 10
    1
      frontend/src/router/index.js
  74. 15
    1
      frontend/src/services/auth.service.js
  75. 4
    1
      frontend/src/services/chat.service.js
  76. 3
    1
      frontend/src/services/grouping.service.js
  77. 1
    0
      frontend/src/services/login.service.js
  78. 31
    2
      frontend/src/services/notification.service.js
  79. 7
    4
      frontend/src/services/profile.service.js
  80. 6
    2
      frontend/src/services/queue.service.js
  81. 20
    2
      frontend/src/services/survey.service.js
  82. 1
    0
      frontend/src/services/user.service.js
  83. 1
    1
      frontend/src/utils/aspects.js
  84. 35
    11
      frontend/src/utils/db.js
  85. 110
    41
      frontend/src/utils/lang.js
  86. 92
    66
      frontend/src/utils/survey.js
  87. 44
    8
      frontend/src/views/ChatView.vue
  88. 28
    4
      frontend/src/views/HomeView.vue
  89. 2
    2
      frontend/src/views/LoginView.vue
  90. 105
    15
      frontend/src/views/OnboardingView.vue
  91. 1
    1
      frontend/src/views/PairsView.vue
  92. 62
    0
      frontend/src/views/SurveyCompleteView.vue
  93. 27
    4
      frontend/src/views/SurveyView.vue
  94. 77
    0
      frontend/src/views/VerifyView.vue

+ 2
- 1
.gitignore Ver arquivo

@@ -6,4 +6,5 @@ dist-ssr
6 6
 **/.env
7 7
 .nyc_output
8 8
 **/generated/_*
9
-**/.vscode
9
+**/.vscode
10
+**/*.vim

+ 4
- 0
backend/.env.sample Ver arquivo

@@ -13,6 +13,7 @@ DB_TYPE=mysql
13 13
 # Extra pepper for auth encryption
14 14
 PEPPER=kosho
15 15
 APP_SECRET=mysecret
16
+APP_SESSION_SALT=somerandomstring
16 17
 
17 18
 
18 19
 # Config for local test dB
@@ -32,3 +33,6 @@ PSCALE_DB_BRANCH=main
32 33
 
33 34
 PSCALE_DB_USER=myuserpleasechange
34 35
 PSCALE_DB_PASSWORD=pscale_pw_abc123efg456hij789
36
+
37
+# Brevo Transactional Email API key
38
+BREVO_KEY=brevo_api_key

+ 14
- 8
backend/db/data-generator/config.json Ver arquivo

@@ -2,7 +2,7 @@
2 2
     "mockOutputPath": "./db/generated",
3 3
     "magic": 1000,
4 4
     "total": 100,
5
-    "ignore": [45],
5
+    "ignore": [],
6 6
     "batchSize": 10,
7 7
     "percentageOfSeekers": 90,
8 8
     "scoreVals": [1, 2, 3, 4, 5, 6, 7],
@@ -30,11 +30,17 @@
30 30
         "91401",
31 31
         "97075"
32 32
     ],
33
-    "resKeys": [1, 2, 3, 4, 5, 6],
34
-    "zipcodeKey": 7,
35
-    "mediaKey": 8,
36
-    "langKey": 9,
37
-    "blurbKey": 12,
38
-    "prefKeys": [7, 10, 11, 13, 14, 15, 16],
39
-    "maxDistanceKey": 16
33
+    "scoreKeys": [1, 2, 3, 4, 5, 6],
34
+    "nameKey": 7,
35
+    "emailKey": 8,
36
+    "passwordKey": 9,
37
+    "zipcodeKey": 10,
38
+    "mediaKey": 12,
39
+    "langKey": 13,
40
+    "durationKey": 14,
41
+    "presenceKey": 15,
42
+    "blurbKey": 16,
43
+    "urgencyKey": 17,
44
+    "pronounsKey": 18,
45
+    "distanceKey": 19
40 46
 }

+ 21
- 22
backend/db/data-generator/index.js Ver arquivo

@@ -115,7 +115,8 @@ const generateResponses = profiles => {
115 115
     // Generate responses first, before filling in details
116 116
     let responses = generate(
117 117
         classes.Response,
118
-        (config.batchSize + extraProfilesToGenerate) * mock.response_keys.length,
118
+        (config.batchSize + extraProfilesToGenerate) *
119
+            mock.response_keys.length,
119 120
         { starting: generatedResponseCount + config.batchSize },
120 121
     )
121 122
     profiles.forEach((profile, i) => {
@@ -125,29 +126,27 @@ const generateResponses = profiles => {
125 126
             resToEdit.response_key_id = k + 1
126 127
             resToEdit.profile_id = profile.profile_id
127 128
 
128
-            if(resToEdit.response_key_id < config.zipcodeKey) {
129
+            if (resToEdit.response_key_id < config.zipcodeKey) {
129 130
                 resToEdit.val = random.valFrom(Object.values(possibleResponses))
130
-            }
131
-            else if(resToEdit.response_key_id == config.zipcodeKey) {
131
+            } else if (resToEdit.response_key_id == config.zipcodeKey) {
132 132
                 resToEdit.val = random.valFrom(config.possibleZipcodes)
133
-            }
134
-            else {
133
+            } else {
135 134
                 switch (resToEdit.response_key_id) {
136
-                case config.mediaKey:
137
-                    resToEdit.val = random.media()
138
-                    break
139
-                case config.langKey:
140
-                    resToEdit.val = random.language()
141
-                    break
142
-                case 10:
143
-                    resToEdit.val = random.duration()
144
-                    break
145
-                case 11:
146
-                    resToEdit.val = random.location()
147
-                    break
148
-                case config.blurbKey:
149
-                    resToEdit.val = random.blurb()
150
-                    break
135
+                    case config.mediaKey:
136
+                        resToEdit.val = random.media()
137
+                        break
138
+                    case config.langKey:
139
+                        resToEdit.val = random.language()
140
+                        break
141
+                    case config.durationKey:
142
+                        resToEdit.val = random.duration()
143
+                        break
144
+                    case config.presenceKey:
145
+                        resToEdit.val = random.location()
146
+                        break
147
+                    case config.blurbKey:
148
+                        resToEdit.val = random.blurb()
149
+                        break
151 150
                 }
152 151
             }
153 152
         }
@@ -170,7 +169,7 @@ for (
170 169
     let jobPosterIds = users
171 170
         .filter(user => user.is_poster > 0)
172 171
         .map(user => user.user_id)
173
-    // Guarentee ONE job poster
172
+    // Guarantee ONE job poster
174 173
     if (!jobPosterIds.length) {
175 174
         random.valFrom(users).is_poster = 1
176 175
         jobPosterIds = users

+ 433
- 39
backend/db/data-generator/mock.js Ver arquivo

@@ -50,6 +50,36 @@ module.exports = {
50 50
             tag_description: 'user_email',
51 51
             is_active: true,
52 52
         },
53
+        {
54
+            tag_id: 9,
55
+            tag_category: 'healthcare--certification',
56
+            tag_description: 'ADM-BC',
57
+            is_active: true,
58
+        },
59
+        {
60
+            tag_id: 10,
61
+            tag_category: 'healthcare--certification',
62
+            tag_description: 'CBHC',
63
+            is_active: true,
64
+        },
65
+        {
66
+            tag_id: 11,
67
+            tag_category: 'healthcare--certification',
68
+            tag_description: 'CEN',
69
+            is_active: true,
70
+        },
71
+        {
72
+            tag_id: 12,
73
+            tag_category: 'healthcare--certification',
74
+            tag_description: 'CFRN',
75
+            is_active: true,
76
+        },
77
+        {
78
+            tag_id: 13,
79
+            tag_category: 'healthcare--certification',
80
+            tag_description: 'CMCN',
81
+            is_active: true,
82
+        },
53 83
     ],
54 84
     tag_associations: [
55 85
         {
@@ -84,7 +114,7 @@ module.exports = {
84 114
             tag_association_id: 5,
85 115
             profile_id: 45,
86 116
             grouping_id: 2,
87
-            tag_id: 4,
117
+            tag_id: 8,
88 118
             is_deleted: false,
89 119
         },
90 120
         {
@@ -115,113 +145,477 @@ module.exports = {
115 145
             tag_id: 5,
116 146
             is_deleted: false,
117 147
         },
148
+        {
149
+            tag_association_id: 10,
150
+            profile_id: 4,
151
+            grouping_id: null,
152
+            tag_id: 1,
153
+            is_deleted: false,
154
+        },
155
+        {
156
+            tag_association_id: 11,
157
+            profile_id: 5,
158
+            grouping_id: null,
159
+            tag_id: 1,
160
+            is_deleted: false,
161
+        },
162
+        {
163
+            tag_association_id: 12,
164
+            profile_id: 6,
165
+            grouping_id: null,
166
+            tag_id: 3,
167
+            is_deleted: false,
168
+        },
169
+        {
170
+            tag_association_id: 13,
171
+            profile_id: 7,
172
+            grouping_id: null,
173
+            tag_id: 8,
174
+            is_deleted: false,
175
+        },
176
+        {
177
+            tag_association_id: 14,
178
+            profile_id: 8,
179
+            grouping_id: null,
180
+            tag_id: 7,
181
+            is_deleted: false,
182
+        },
183
+        {
184
+            tag_association_id: 15,
185
+            profile_id: 9,
186
+            grouping_id: 1,
187
+            tag_id: 2,
188
+            is_deleted: false,
189
+        },
190
+        {
191
+            tag_association_id: 16,
192
+            profile_id: 10,
193
+            grouping_id: 2,
194
+            tag_id: 2,
195
+            is_deleted: false,
196
+        },
197
+        {
198
+            tag_association_id: 17,
199
+            profile_id: 11,
200
+            grouping_id: 2,
201
+            tag_id: 3,
202
+            is_deleted: false,
203
+        },
204
+        {
205
+            tag_association_id: 18,
206
+            profile_id: 12,
207
+            grouping_id: 3,
208
+            tag_id: 4,
209
+            is_deleted: false,
210
+        },
211
+        {
212
+            tag_association_id: 19,
213
+            profile_id: 13,
214
+            grouping_id: 3,
215
+            tag_id: 7,
216
+            is_deleted: false,
217
+        },
218
+        {
219
+            tag_association_id: 20,
220
+            profile_id: 14,
221
+            grouping_id: 4,
222
+            tag_id: 6,
223
+            is_deleted: false,
224
+        },
225
+        {
226
+            tag_association_id: 21,
227
+            profile_id: 15,
228
+            grouping_id: 1,
229
+            tag_id: 1,
230
+            is_deleted: false,
231
+        },
232
+        {
233
+            tag_association_id: 22,
234
+            profile_id: 16,
235
+            grouping_id: 1,
236
+            tag_id: 2,
237
+            is_deleted: false,
238
+        },
239
+        {
240
+            tag_association_id: 23,
241
+            profile_id: 17,
242
+            grouping_id: null,
243
+            tag_id: 6,
244
+            is_deleted: false,
245
+        },
246
+        {
247
+            tag_association_id: 24,
248
+            profile_id: 18,
249
+            grouping_id: null,
250
+            tag_id: 8,
251
+            is_deleted: false,
252
+        },
253
+        {
254
+            tag_association_id: 25,
255
+            profile_id: 19,
256
+            grouping_id: null,
257
+            tag_id: 3,
258
+            is_deleted: false,
259
+        },
260
+        {
261
+            tag_association_id: 26,
262
+            profile_id: 20,
263
+            grouping_id: null,
264
+            tag_id: 3,
265
+            is_deleted: false,
266
+        },
267
+        {
268
+            tag_association_id: 27,
269
+            profile_id: 21,
270
+            grouping_id: 2,
271
+            tag_id: 1,
272
+            is_deleted: false,
273
+        },
274
+        {
275
+            tag_association_id: 28,
276
+            profile_id: 22,
277
+            grouping_id: 2,
278
+            tag_id: 1,
279
+            is_deleted: false,
280
+        },
281
+        {
282
+            tag_association_id: 29,
283
+            profile_id: 23,
284
+            grouping_id: 3,
285
+            tag_id: 2,
286
+            is_deleted: false,
287
+        },
288
+        {
289
+            tag_association_id: 30,
290
+            profile_id: 24,
291
+            grouping_id: 4,
292
+            tag_id: 4,
293
+            is_deleted: false,
294
+        },
295
+        {
296
+            tag_association_id: 31,
297
+            profile_id: 25,
298
+            grouping_id: 5,
299
+            tag_id: 3,
300
+            is_deleted: false,
301
+        },
302
+        {
303
+            tag_association_id: 32,
304
+            profile_id: 26,
305
+            grouping_id: 1,
306
+            tag_id: 4,
307
+            is_deleted: false,
308
+        },
309
+        {
310
+            tag_association_id: 33,
311
+            profile_id: 27,
312
+            grouping_id: null,
313
+            tag_id: 3,
314
+            is_deleted: false,
315
+        },
316
+        {
317
+            tag_association_id: 34,
318
+            profile_id: 28,
319
+            grouping_id: null,
320
+            tag_id: 3,
321
+            is_deleted: false,
322
+        },
323
+        {
324
+            tag_association_id: 35,
325
+            profile_id: 29,
326
+            grouping_id: null,
327
+            tag_id: 7,
328
+            is_deleted: false,
329
+        },
330
+        {
331
+            tag_association_id: 36,
332
+            profile_id: 30,
333
+            grouping_id: null,
334
+            tag_id: 6,
335
+            is_deleted: false,
336
+        },
337
+        {
338
+            tag_association_id: 37,
339
+            profile_id: 31,
340
+            grouping_id: 3,
341
+            tag_id: 4,
342
+            is_deleted: false,
343
+        },
344
+        {
345
+            tag_association_id: 38,
346
+            profile_id: 32,
347
+            grouping_id: 3,
348
+            tag_id: 5,
349
+            is_deleted: false,
350
+        },
351
+        {
352
+            tag_association_id: 39,
353
+            profile_id: 33,
354
+            grouping_id: 3,
355
+            tag_id: 5,
356
+            is_deleted: false,
357
+        },
358
+        {
359
+            tag_association_id: 40,
360
+            profile_id: 34,
361
+            grouping_id: 4,
362
+            tag_id: 7,
363
+            is_deleted: false,
364
+        },
365
+        {
366
+            tag_association_id: 41,
367
+            profile_id: 35,
368
+            grouping_id: 4,
369
+            tag_id: 8,
370
+            is_deleted: false,
371
+        },
372
+        {
373
+            tag_association_id: 42,
374
+            profile_id: 36,
375
+            grouping_id: 1,
376
+            tag_id: 1,
377
+            is_deleted: false,
378
+        },
379
+        {
380
+            tag_association_id: 43,
381
+            profile_id: 37,
382
+            grouping_id: 1,
383
+            tag_id: 2,
384
+            is_deleted: false,
385
+        },
386
+        {
387
+            tag_association_id: 44,
388
+            profile_id: 38,
389
+            grouping_id: null,
390
+            tag_id: 2,
391
+            is_deleted: false,
392
+        },
393
+        {
394
+            tag_association_id: 45,
395
+            profile_id: 39,
396
+            grouping_id: null,
397
+            tag_id: 6,
398
+            is_deleted: false,
399
+        },
400
+        {
401
+            tag_association_id: 46,
402
+            profile_id: 40,
403
+            grouping_id: null,
404
+            tag_id: 5,
405
+            is_deleted: false,
406
+        },
407
+        {
408
+            tag_association_id: 47,
409
+            profile_id: 41,
410
+            grouping_id: null,
411
+            tag_id: 6,
412
+            is_deleted: false,
413
+        },
414
+        {
415
+            tag_association_id: 48,
416
+            profile_id: 42,
417
+            grouping_id: 1,
418
+            tag_id: 8,
419
+            is_deleted: false,
420
+        },
421
+        {
422
+            tag_association_id: 49,
423
+            profile_id: 43,
424
+            grouping_id: 1,
425
+            tag_id: 7,
426
+            is_deleted: false,
427
+        },
118 428
     ],
119 429
     response_keys: [
120 430
         {
121 431
             response_key_id: 1,
122 432
             response_key_category: 'visionary_vs_implementer',
123
-            response_key_prompt:
124
-                'While managing your team, do you find success in your employees being more Visionary or Implementer?',
433
+            response_key_prompt: 'Do you prefer to work with those who are driven by their Visionary insights, or those who are driven more by their Implementation?',
125 434
             response_key_description: 'first round draft scoring question',
435
+            aspect: 'visionary_vs_implementer',
436
+            category: 'aspect',
437
+            placeholder: null,
438
+            invalidInputPrompt: null,
126 439
         },
127 440
         {
128 441
             response_key_id: 2,
129 442
             response_key_category: 'creative_vs_methodical',
130
-            response_key_prompt:
131
-                'In your department, do you find more success in your employees being Creative or Methodical in their job duties?',
443
+            response_key_prompt: 'Have you found more success working with employees that are more Creative or those that are more Methodical?',
132 444
             response_key_description: 'first round draft scoring question',
445
+            aspect: 'creative_vs_methodical',
446
+            category: 'aspect',
447
+            placeholder: null,
448
+            invalidInputPrompt: null,
133 449
         },
134 450
         {
135 451
             response_key_id: 3,
136
-            response_key_category: 'collaborative_vs_independent',
137
-            response_key_prompt:
138
-                'Do you structure and encourage your team to be a Collaborative or Independent environment?',
452
+            response_key_category: 'dynamic_vs_ordered',
453
+            response_key_prompt: 'Which do you find to be the ideal working environment, one that is more Collaborative or one that is more Independent?',
139 454
             response_key_description: 'first round draft scoring question',
455
+            aspect: 'dynamic_vs_ordered',
456
+            category: 'aspect',
457
+            placeholder: null,
458
+            invalidInputPrompt: null,
140 459
         },
141 460
         {
142 461
             response_key_id: 4,
143
-            response_key_category: 'innovative_vs_conventional',
144
-            response_key_prompt:
145
-                'Do you find higher success in employees on your team that are Innovative or Conventional?',
462
+            response_key_category: 'precise_vs_resourceful',
463
+            response_key_prompt: 'Is the success of your team more likely if it includes individuals who are more Innovative, or those that are more Conventional when fulfilling their job duties?',
146 464
             response_key_description: 'first round draft scoring question',
465
+            aspect: 'precise_vs_resourceful',
466
+            category: 'aspect',
467
+            placeholder: null,
468
+            invalidInputPrompt: null,
147 469
         },
148 470
         {
149 471
             response_key_id: 5,
150 472
             response_key_category: 'big_Picture_vs_focused',
151
-            response_key_prompt:
152
-                'As a hiring leader, are you a Big Picture or Focused thinker when it comes to how you operate in your job duties?',
473
+            response_key_prompt: 'When fulfilling the role of the hiring leader, do you find yourself focusing more on the Big Picture or The Task At Hand?',
153 474
             response_key_description: 'first round draft scoring question',
475
+            aspect: 'big_Picture_vs_focused',
476
+            category: 'aspect',
477
+            placeholder: null,
478
+            invalidInputPrompt: null,
154 479
         },
155 480
         {
156 481
             response_key_id: 6,
157 482
             response_key_category: 'guided_vs_self-managed',
158
-            response_key_prompt:
159
-                'Do you prefer your employees to be Guided or Self-managed in achieving completion of their responsibilities?',
483
+            response_key_prompt: 'Do you prefer to Guide your employees towards achieving the team goals, or do you prefer your employees to be Self-Managed?',
160 484
             response_key_description: 'first round draft scoring question',
485
+            aspect: 'guided_vs_self-managed',
486
+            category: 'aspect',
487
+            placeholder: null,
488
+            invalidInputPrompt: null,
161 489
         },
162 490
         {
163 491
             response_key_id: 7,
164 492
             response_key_category: 'profile',
165
-            response_key_prompt: 'zipcode',
166
-            response_key_description: 'required for distance calculations',
493
+            response_key_prompt: 'First things first, could you provide us with your name? [break] I am called [break] when others address me.',
494
+            response_key_description: 'required for profile creation',
495
+            aspect: null,
496
+            category: 'input',
497
+            placeholder: 'Joe Doe',
498
+            invalidInputPrompt: 'So sorry, but what is your name?',
167 499
         },
168 500
         {
169 501
             response_key_id: 8,
170 502
             response_key_category: 'profile',
171
-            response_key_prompt: 'image',
172
-            response_key_description: 'required for profile pictures',
503
+            response_key_prompt: 'In order for others to reach out to you on Siimee, we will need you to provide your email address.[break]When reaching out to me, [break] is my preferred email.',
504
+            response_key_description: 'required for profile creation',
505
+            aspect: null,
506
+            category: 'input',
507
+            placeholder: 'joe@mailme.com',
508
+            invalidInputPrompt: 'It looks like that email is not valid, try en email that is formatted like so: joe@joe.com',
173 509
         },
174 510
         {
175 511
             response_key_id: 9,
176 512
             response_key_category: 'profile',
177
-            response_key_prompt: 'language',
178
-            response_key_description:
179
-                'programming and spoken language preference',
513
+            response_key_prompt: 'So far so good! Next we will need you to establish a super secret password. Your password should be at least 14 characters long and have at least 2 special characters.[break]My [break] is a very secure passcode that only I will have access to!',
514
+            response_key_description: 'required for profile creation',
515
+            aspect: null,
516
+            category: 'input',
517
+            placeholder: 'supersecr3tp@ssword',
518
+            invalidInputPrompt: 'That password does not fit our requirements, please follow the above instructions to generate a secure password.',
180 519
         },
181 520
         {
182 521
             response_key_id: 10,
183 522
             response_key_category: 'profile',
184
-            response_key_prompt: 'duration',
185
-            response_key_description:
186
-                'duration preference for hours able to dedicate to work',
523
+            response_key_prompt: 'Looking good! Doing great. The next piece of info needed is your zip code. That way we can be sure to only show you other people in your area.[break]My zip code, [break] is the general area where I wish to see results in.',
524
+            response_key_description: 'required for distance calculations',
525
+            aspect: null,
526
+            category: 'input',
527
+            placeholder: '90012',
528
+            invalidInputPrompt: 'Oops! That is not a recognized zipcode, please enter a 5 digit zipcode like: 97869',
187 529
         },
188 530
         {
189 531
             response_key_id: 11,
190 532
             response_key_category: 'profile',
191
-            response_key_prompt: 'presence',
192
-            response_key_description:
193
-                'location preference for where work happens',
533
+            response_key_prompt: 'What are you seeking? Are you looking to find a position to be employed in, or are you looking to employ a candidate?[break] I am a [break] seeking an employer/employee.',
534
+            response_key_description: 'required for profile generation',
535
+            aspect: null,
536
+            category: 'choice',
537
+            placeholder: null,
538
+            invalidInputPrompt: 'In order to provide you with the best results, Siimee will need to know whether you are an employer looking to fill a position, or a candidate looking for an employment. Please take a look at our above options and choose one.',
194 539
         },
195 540
         {
196 541
             response_key_id: 12,
197 542
             response_key_category: 'profile',
198
-            response_key_prompt: 'blurb',
199
-            response_key_description: 'required for profile description',
543
+            response_key_prompt: 'Hey, you are almost done! Please provide an image of yourself so others can recognize you if you ever meet up IRL:',
544
+            response_key_description: 'required for profile pictures',
545
+            aspect: null,
546
+            category: 'input',
547
+            placeholder: null,
548
+            invalidInputPrompt: 'It appears you have yet to upload an image. Please provide Siimee with an image in case you want to show others what you look like.',
200 549
         },
201 550
         {
202 551
             response_key_id: 13,
203 552
             response_key_category: 'profile',
204
-            response_key_prompt: 'urgency',
205
-            response_key_description: 'urgency for when work is required',
553
+            response_key_prompt: 'What language is your native language?[break] I consider [break] language as my native language.',
554
+            response_key_description: 'programming and spoken language preference',
555
+            aspect: null,
556
+            category: 'choice',
557
+            placeholder: null,
558
+            invalidInputPrompt: 'We try our best to provide results in the language of your choosing. ¿Prefieres ver resultados en español? Ou peut-être parlez-vous français? Or would you prefer to see results in english?',
206 559
         },
207 560
         {
208 561
             response_key_id: 14,
209 562
             response_key_category: 'profile',
210
-            response_key_prompt: 'role',
211
-            response_key_description: 'current and desired role',
563
+            response_key_prompt: 'What kind of duration would you prefer? Are you looking for part-time, full-time, other?[break] Currently, I am looking for a [break] job at this time.',
564
+            response_key_description: 'duration preference for hours able to dedicate to work',
565
+            aspect: null,
566
+            category: 'choice',
567
+            placeholder: null,
568
+            invalidInputPrompt: 'Looks like you have yet to  fill out what kind of work you are most interested in. As in, part-time, full-time. Take a look at our above options and choose whatever feels right for you right now. You can always edit them later!',
212 569
         },
213 570
         {
214 571
             response_key_id: 15,
215 572
             response_key_category: 'profile',
216
-            response_key_prompt: 'pronouns',
217
-            response_key_description: 'required for profile pronouns',
573
+            response_key_prompt: 'Would you prefer remote, hybrid, in-person work?[break] Personally I would prefer a [break] job right now. It is just what works best for me.',
574
+            response_key_description: 'location preference for where work happens',
575
+            aspect: null,
576
+            category: 'choice',
577
+            placeholder: null,
578
+            invalidInputPrompt: 'Hold up! So sorry to put a pause here, but it looks like you have not chosen whether to work remotely or in person. No worries, if you are unsure, just choose the flexible option.',
218 579
         },
219 580
         {
220 581
             response_key_id: 16,
221 582
             response_key_category: 'profile',
222
-            response_key_prompt: 'distance',
223
-            response_key_description:
224
-                'preference for commuting distance cutoff',
583
+            response_key_prompt: 'Please provide us with a short blurb about yourself. What is your backstory?[break] My origin story starts like this:[break]',
584
+            response_key_description: 'required for profile description',
585
+            aspect: null,
586
+            category: 'input',
587
+            placeholder: 'my backstory starts long long ago...',
588
+            invalidInputPrompt: 'Whoa! Cool story. Unfortunately your backstory is either too long or too short. Please tell us a bit about yourself between 1 and 100 characters.',
589
+        },
590
+        {
591
+            response_key_id: 17,
592
+            response_key_category: 'profile',
593
+            response_key_prompt: 'How soon do you need the position filled or you need to be employed? [break]I am currently [break] when it comes to employment opportunities right now.',
594
+            response_key_description: 'urgency for when work is required',
595
+            aspect: null,
596
+            category: 'choice',
597
+            placeholder: null,
598
+            invalidInputPrompt: 'Looks like you left this field blank. Take a look at our provided options and tell us when you would like be employed.',
599
+        },
600
+        {
601
+            response_key_id: 18,
602
+            response_key_category: 'profile',
603
+            response_key_prompt: 'When others refer to you, what pronouns do you prefer they use?[break]I prefer to be called [break] when others refer to me.',
604
+            response_key_description: 'required for profile pronouns',
605
+            aspect: null,
606
+            category: 'choice',
607
+            placeholder: null,
608
+            invalidInputPrompt: 'Ensuring that others on our platform are aware of what your preferred pronouns are is important to us. Please choose from one of the above options.',
609
+        },
610
+        {
611
+            response_key_id: 19,
612
+            response_key_category: 'profile',
613
+            response_key_prompt: 'What distance from your home are you looking to work in?[break] Preferably, I would like to work [break] from my place of residence.',
614
+            response_key_description: 'preference for commuting distance cutoff',
615
+            aspect: null,
616
+            category: 'input',
617
+            placeholder: '5 mi',
618
+            invalidInputPrompt: 'Whoa! You either left this field blank or tried to input an astronomically large distance you would like to see results from. Please input a distance you would like to see results in.',
225 619
         },
226 620
     ],
227 621
     responses: [],

+ 4
- 0
backend/db/migrations/20210527174416_create_response_keys_table.js Ver arquivo

@@ -4,6 +4,10 @@ exports.up = function (knex) {
4 4
         table.string('response_key_category').notNullable()
5 5
         table.string('response_key_prompt')
6 6
         table.string('response_key_description')
7
+        table.string('aspect')
8
+        table.string('category')
9
+        table.string('placeholder')
10
+        table.string('invalidInputPrompt')
7 11
     })
8 12
 }
9 13
 

+ 8
- 6
backend/knexfile.js Ver arquivo

@@ -1,6 +1,8 @@
1 1
 require('dotenv').config()
2 2
 const fs = require('fs')
3 3
 
4
+const useLocalDb = () => process.env.USE_LOCAL_DB == 'true'
5
+
4 6
 const local = {
5 7
     host: process.env.DB_HOST,
6 8
     user: process.env.DB_USER,
@@ -19,12 +21,10 @@ const pscale = {
19 21
     port: process.env.PSCALE_DB_PORT ? process.env.PSCALE_DB_PORT : 3306,
20 22
 }
21 23
 
22
-const connectionSettings = process.env.USE_LOCAL_DB == 'true' ? local : pscale
23
-
24 24
 module.exports = {
25 25
     development: {
26 26
         client: process.env.DB_TYPE,
27
-        connection: connectionSettings,
27
+        connection: useLocalDb() ? local : pscale,
28 28
         pool: {
29 29
             min: 2,
30 30
             max: 10,
@@ -35,8 +35,10 @@ module.exports = {
35 35
         seeds: {
36 36
             directory: './db/seeds',
37 37
         },
38
-        ssl: {
39
-            ca: fs.readFileSync('/etc/ssl/certs/ca-certificates.crt'),
40
-        },
38
+        ssl: useLocalDb()
39
+            ? {}
40
+            : {
41
+                  ca: fs.readFileSync('/etc/ssl/certs/ca-certificates.crt'),
42
+              },
41 43
     },
42 44
 }

+ 61
- 16
backend/lib/auth/strategies/jwt.js Ver arquivo

@@ -1,27 +1,72 @@
1 1
 'use strict'
2
+const JWT = require('jsonwebtoken')
3
+const crypto = require('crypto')
4
+
5
+const hashToken = async token => {
6
+    const salt = process.env.APP_SESSION_SALT
7
+    try {
8
+        return crypto.createHmac('sha256', salt).update(token).digest('hex')
9
+    } catch (err) {
10
+        throw new Error(err.message)
11
+    }
12
+}
13
+
14
+const createToken = (data, expiration = 600) => {
15
+    const key = process.env.APP_SECRET
16
+    const obj = {}
17
+
18
+    Object.assign(obj, { ...data })
19
+    return JWT.sign(obj, key, { expiresIn: expiration })
20
+}
21
+
22
+const validateToken = token => {
23
+    const key = process.env.APP_SECRET
24
+    try {
25
+        return JWT.verify(token, key)
26
+    } catch (err) {
27
+        return { payload: null, message: err.message }
28
+    }
29
+}
2 30
 
3 31
 module.exports = options => {
4 32
     return {
5
-        keys: {
6
-            key: options.jwtKey,
33
+        key: options.jwtKey,
34
+        verifyOptions: {
7 35
             algorithms: ['HS256'],
8 36
         },
9
-        verify: {
10
-            aud: 'urn:audience:test',
11
-            iss: 'urn:issuer:test',
12
-            sub: false,
13
-        },
14
-        validate: (artifacts, request, h) => {
37
+        // TODO: Naming conventions need to be reversed again??
38
+        validate: async (decoded, request, h) => {
39
+            const accessTokenFromHeaders = request.headers.authorization
40
+            const hashedAccessTokenFromHeaders = await hashToken(
41
+                accessTokenFromHeaders,
42
+            )
43
+            const activeSession =
44
+                request.server.app.activeSessions[hashedAccessTokenFromHeaders]
45
+            if (!activeSession)
46
+                throw new Error(
47
+                    `No session found for ${hashedAccessTokenFromHeaders}`,
48
+                )
49
+
50
+            const accessToken = activeSession.accessToken
51
+            const sessionToken = activeSession.sessionToken
52
+            const validatedAccessToken = validateToken(accessToken)
53
+            const validatedSessionToken = validateToken(sessionToken)
54
+            if (!validatedSessionToken.payload) {
55
+                console.log('sessionToken no longer valid, reissuing... ')
56
+                activeSession.sessionToken = createToken(
57
+                    { payload: validatedAccessToken.payload },
58
+                    // NOTE: Expiration of new sessionToken set for 200 seconds (testing)
59
+                    100,
60
+                )
61
+            }
15 62
             try {
16
-                return {
17
-                    isValid: true,
18
-                    credentials: { user: artifacts.decoded.payload.user },
19
-                }
63
+                const validatedJwt = JWT.verify(
64
+                    accessToken,
65
+                    process.env.APP_SECRET,
66
+                )
67
+                return { isValid: true, credentials: validatedJwt.email }
20 68
             } catch (err) {
21
-                console.error(err)
22
-                return {
23
-                    isValid: false,
24
-                }
69
+                return { isValid: false, error: err.message }
25 70
             }
26 71
         },
27 72
     }

+ 2
- 0
backend/lib/plugins/profile.js Ver arquivo

@@ -16,6 +16,7 @@ const MatchService = require('../services/match')
16 16
 
17 17
 const ProfileScoreRoute = require('../routes/profile/score')
18 18
 const ProfileUpdateRoute = require('../routes/profile/update')
19
+const ProfileInsertRoute = require('../routes/profile/insert')
19 20
 const ProfileRespondRoute = require('../routes/profile/respond')
20 21
 const ProfileMatchRoute = require('../routes/profile/match')
21 22
 const ProfileQueueRoute = require('../routes/profile/queue')
@@ -51,6 +52,7 @@ module.exports = {
51 52
         await server.route(ProfileScoreRoute)
52 53
         await server.route(ProfileRespondRoute)
53 54
         await server.route(ProfileUpdateRoute)
55
+        await server.route(ProfileInsertRoute)
54 56
         await server.route(ProfileMatchRoute)
55 57
         await server.route(ProfileQueueRoute)
56 58
         await server.route(ProfileGetRoute)

+ 9
- 1
backend/lib/plugins/user.js Ver arquivo

@@ -1,7 +1,7 @@
1 1
 const Objection = require('objection')
2 2
 const Schmervice = require('@hapipal/schmervice')
3 3
 const Schwifty = require('@hapipal/schwifty')
4
-const Jwt = require('@hapi/jwt')
4
+const Jwt = require('hapi-auth-jwt2')
5 5
 const JwtStrategy = require('../auth/strategies/jwt')
6 6
 
7 7
 const UserModel = require('../models/user')
@@ -12,6 +12,10 @@ const UserProfileCreateRoute = require('../routes/user/create-profile')
12 12
 const UserProfilesListRoute = require('../routes/user/list-profiles')
13 13
 const UserLoginRoute = require('../routes/user/login')
14 14
 const UserSignupRoute = require('../routes/user/signup')
15
+const UserEmailRoute = require('../routes/user/email.js')
16
+const UserVerifyActiveRoute = require('../routes/user/verifyactivesession.js')
17
+const UserGetAccessRoute = require('../routes/user/getaccess.js')
18
+const UserValidateSessionRoute = require('../routes/user/validatesession.js')
15 19
 const UserPassword = require('../routes/user/authentication')
16 20
 
17 21
 const UserService = require('../services/user')
@@ -49,6 +53,10 @@ module.exports = {
49 53
         await server.route(UserSignupRoute)
50 54
         await server.route(UserProfileCreateRoute)
51 55
         await server.route(UserProfilesListRoute)
56
+        await server.route(UserEmailRoute)
57
+        await server.route(UserVerifyActiveRoute)
58
+        await server.route(UserGetAccessRoute)
59
+        await server.route(UserValidateSessionRoute)
52 60
         await server.route(UserPassword)
53 61
     },
54 62
 }

+ 49
- 25
backend/lib/routes/membership/active.js Ver arquivo

@@ -62,48 +62,72 @@ module.exports = {
62 62
         auth: false,
63 63
         cors: true,
64 64
         handler: async function (request, h) {
65
-            const { membershipService, profileService } =
65
+            const { membershipService, profileService, userService } =
66 66
                 request.server.services()
67 67
             const membershipType = request.query.type
68 68
 
69 69
             const profileId = request.params.profile_id
70
-            let groupings = await membershipService.findGroupingsByProfileId(
70
+            const groupings = await membershipService.findGroupingsByProfileId(
71 71
                 profileId,
72 72
                 membershipType,
73 73
             )
74
-            let memberships = await membershipService.findMemberships(
75
-                groupings.map(grouping => grouping.grouping_id),
74
+            const groupingIds = groupings.map(grouping => grouping.grouping_id)
75
+            const memberships = await membershipService.findMemberships(
76
+                groupingIds,
77
+            )
78
+            const profileIds = memberships
79
+                .filter(membership => membership.profile_id != profileId)
80
+                .map(membership => membership.profile_id)
81
+
82
+            /** Assemble complete profiles to reference and pass */
83
+            const completedProfiles = await profileService.getProfilesFor(
84
+                profileIds,
85
+                'participant',
76 86
             )
77 87
 
78 88
             /**
79 89
              * Heavily process the result by storing just a profile_id
80 90
              * and attach complete profiles
91
+             * !: This still assumes only ONE other profile
92
+             * TODO: should be refactored to many other profiles
81 93
              */
82
-            let pIds = groupings.reduce((ids, grouping) => {
83
-                grouping.profiles.forEach(p => {
84
-                    if (p.profile_id == profileId) return
85
-                    ids.push(p.profile_id)
86
-                    grouping.profile = p.profile_id
87
-                })
94
+            const reformattedGroupings = groupings.map(grouping => {
95
+                const otherPid = grouping.profiles.find(
96
+                    p => p.profile_id != profileId,
97
+                ).profile_id
98
+                grouping.profile = completedProfiles.find(
99
+                    p => otherPid == p.profile_id,
100
+                )
101
+                grouping.is_paired = _activeGroupingIds(memberships).includes(
102
+                    grouping.grouping_id,
103
+                )
88 104
                 delete grouping.profiles
89
-                return ids
90
-            }, [])
105
+                return grouping
106
+            })
91 107
 
92
-            /** Assemble complete profiles to reference and pass */
93
-            const completedProfiles = await profileService.getProfilesFor(
94
-                pIds,
95
-                'participant',
96
-                false,
108
+            /** Grabs revealTags */
109
+            const revealTags = await profileService.getTagsFor(
110
+                profileIds,
111
+                groupingIds,
112
+                'reveal',
113
+            )
114
+
115
+            /** If the revealTags exist, the completedProfile's hidden info is
116
+             * removed and replaced with the completedProfile's user information
117
+             * Otherwise the completedProfiles remain unchanged
118
+             */
119
+            const user = await userService.findById(
120
+                completedProfiles.map(p => p.user_id),
97 121
             )
98
-            const reformattedGroupings = groupings.map(g => {
99
-                completedProfiles.forEach(p => {
100
-                    g.profile = g.profile == p.profile_id ? p : g.profile
122
+
123
+            // TODO: Refactor this. Is it safe to always use completedProfiles[0]?
124
+            if (revealTags && user) {
125
+                revealTags.forEach(t => {
126
+                    if (!t.tag.tag_description) return
127
+                    completedProfiles[0][t.tag.tag_description] =
128
+                        user[t.tag.tag_description]
101 129
                 })
102
-                g.is_paired = _activeGroupingIds(memberships).includes(
103
-                    g.grouping_id,
104
-                )
105
-                return g
106
-            })
130
+            }
107 131
 
108 132
             try {
109 133
                 return {

+ 5
- 0
backend/lib/routes/membership/join.js Ver arquivo

@@ -76,6 +76,7 @@ module.exports = {
76 76
                         groupingToWrite,
77 77
                         role,
78 78
                     )
79
+
79 80
                 const hasMatch = memberships.every(
80 81
                     membership => membership && membership.is_active == true,
81 82
                 )
@@ -85,6 +86,8 @@ module.exports = {
85 86
                         `${profileId}.stonk`,
86 87
                         {
87 88
                             name: `${res.target_id} Match Fffound`,
89
+                            // TODO: add urls for chat
90
+                            url: `<a href="/profile/${res.target_id}">url</a>`,
88 91
                             type: 'info',
89 92
                         },
90 93
                         h,
@@ -93,6 +96,8 @@ module.exports = {
93 96
                         `${res.target_id}.stonk`,
94 97
                         {
95 98
                             name: `${profileId} Match Fffound`,
99
+                            // TODO: add urls for chat
100
+                            url: `<a href="/profile/${profileId}">url</a>`,
96 101
                             type: 'info',
97 102
                         },
98 103
                         h,

+ 37
- 4
backend/lib/routes/membership/reveal.js Ver arquivo

@@ -31,12 +31,12 @@ module.exports = {
31 31
         auth: false,
32 32
         cors: true,
33 33
         handler: async function (request, h) {
34
-            const { membershipService, profileService } =
34
+            const { membershipService, profileService, userService } =
35 35
                 request.server.services()
36 36
             const grouping_id = request.params.grouping_id
37 37
             const { profile_id, tag_id } = request.query
38 38
             try {
39
-                const tags = await profileService.revealProfileInfo({
39
+                const associations = await profileService.revealProfileInfo({
40 40
                     profile_id,
41 41
                     grouping_id,
42 42
                     tag_id,
@@ -50,13 +50,46 @@ module.exports = {
50 50
                 const idsInGroup = memberships.map(
51 51
                     membership => membership.profile_id,
52 52
                 )
53
+                if (idsInGroup.length > 2)
54
+                    return console.error(
55
+                        'ERROR: idsInGroup cannot have more than 2 entries: ',
56
+                        idsInGroup,
57
+                    )
58
+                // Grab User Info from Users Table
59
+                const completeProfile = await profileService.getProfilesFor(
60
+                    [profile_id],
61
+                    'participant',
62
+                )
63
+                const userInfo = await userService.findById(
64
+                    completeProfile[0].user_id,
65
+                )
66
+
67
+                // Grab the TagAssociation that matches the revealed profile
68
+                // TODO: Check if there are multiple matching associations(?)(there shouldn't be)
69
+                let matchingAssociation = null
70
+                associations.forEach(tagAssoc => {
71
+                    if (
72
+                        tagAssoc.grouping_id === grouping_id &&
73
+                        tagAssoc.profile_id === profile_id &&
74
+                        tagAssoc.tag_id === tag_id
75
+                    ) {
76
+                        matchingAssociation = tagAssoc
77
+                    }
78
+                })
79
+                const description = matchingAssociation.tag.tag_description
80
+
53 81
                 idsInGroup.forEach(profile_id => {
54 82
                     request.server.methods.notify(
55 83
                         `${profile_id}.stonk`,
56 84
                         {
57
-                            name: 'REVEALED INFO',
85
+                            name: 'REVEALED_INFO',
86
+                            revealed_info: userInfo[description],
87
+                            profile_id: completeProfile[0].profile_id,
88
+                            grouping_id: grouping_id,
58 89
                             tag: tag_id,
90
+                            description,
59 91
                             type: 'info',
92
+                            url: `<a href="/chat/${profile_id}">url</a>`
60 93
                         },
61 94
                         h,
62 95
                     )
@@ -65,7 +98,7 @@ module.exports = {
65 98
                     .response({
66 99
                         ok: true,
67 100
                         handler: pluginConfig.handlerType,
68
-                        data: { tags },
101
+                        data: { tags: associations },
69 102
                     })
70 103
                     .code(200)
71 104
             } catch (err) {

+ 1
- 2
backend/lib/routes/profile/get.js Ver arquivo

@@ -28,8 +28,7 @@ module.exports = {
28 28
     options: {
29 29
         ...pluginConfig.docs,
30 30
         tags: ['api'],
31
-        /** Protect this route with authentication? */
32
-        auth: false,
31
+        auth: 'default_jwt',
33 32
         cors: true,
34 33
         handler: async function (request, h) {
35 34
             const { profile_id } = request.params

+ 85
- 0
backend/lib/routes/profile/insert.js Ver arquivo

@@ -0,0 +1,85 @@
1
+'use strict'
2
+
3
+const Joi = require('joi')
4
+const apiSchema = require('../../schemas/api')
5
+const errorSchema = require('../../schemas/errors')
6
+const surveyResponseSchema = require('../../schemas/responses')
7
+const params = require('../../schemas/params')
8
+
9
+const pluginConfig = {
10
+    handlerType: 'profile',
11
+    docs: {
12
+        description: 'Insert responses',
13
+        notes: 'Insert new responses',
14
+    },
15
+}
16
+
17
+const responseSchemas = {
18
+    response: surveyResponseSchema.single,
19
+    error: errorSchema.single,
20
+}
21
+
22
+module.exports = {
23
+    method: 'POST',
24
+    path: '/{profile_id}/insert/{response_key_id?}',
25
+    options: {
26
+        ...pluginConfig.docs,
27
+        tags: ['api'],
28
+        /** Protect this route with authentication? */
29
+        auth: false,
30
+        cors: true,
31
+
32
+        handler: async function (request, h) {
33
+            const { profileService } = request.services()
34
+            /** Grab payload info */
35
+            const res = request.payload
36
+
37
+            try {
38
+                // TODO: Currently passwords are stored in plain text, big no no...
39
+                const insertedResponse =
40
+                    await profileService.insertSingleResponseForProfile(res)
41
+
42
+                if (!insertedResponse) {
43
+                    throw new Error('Response not inserted')
44
+                }
45
+
46
+                return h
47
+                    .response({
48
+                        ok: true,
49
+                        handler: pluginConfig.handlerType,
50
+                        data: insertedResponse,
51
+                    })
52
+                    .code(200)
53
+            } catch (err) {
54
+                return h
55
+                    .response({
56
+                        ok: false,
57
+                        handler: pluginConfig.handlerType,
58
+                        data: { error: `${err}` },
59
+                    })
60
+                    .code(409)
61
+            }
62
+        },
63
+
64
+        /** Validate based on validators object */
65
+        validate: {
66
+            failAction: 'log',
67
+        },
68
+
69
+        /** Validate the server response */
70
+        response: {
71
+            status: {
72
+                200: apiSchema.single
73
+                    .append({
74
+                        data: responseSchemas.response,
75
+                    })
76
+                    .label('response_list_res'),
77
+                409: apiSchema.single
78
+                    .append({
79
+                        data: responseSchemas.error,
80
+                    })
81
+                    .label('error_single_res'),
82
+            },
83
+        },
84
+    },
85
+}

+ 21
- 17
backend/lib/routes/profile/queue.js Ver arquivo

@@ -23,7 +23,11 @@ const responseSchemas = {
23 23
 
24 24
 const validators = {
25 25
     params: params.profileId,
26
-    query: params.profileInclude,
26
+    query: Joi.object({
27
+        include_profile: Joi.bool(),
28
+        limit: Joi.number(),
29
+        offset: Joi.number(),
30
+    }),
27 31
 }
28 32
 
29 33
 module.exports = {
@@ -37,18 +41,16 @@ module.exports = {
37 41
         cors: true,
38 42
         handler: async function (request, h) {
39 43
             const { profile_id } = request.params
40
-            const { include_profile } = request.query
44
+            const { limit, offset } = request.query
41 45
             const { profileService, matchQueueService } =
42 46
                 request.server.services()
43 47
 
44
-            const queue = await matchQueueService.getQueue(profile_id)
48
+            const queue = await matchQueueService.getQueue(
49
+                profile_id,
50
+                limit,
51
+                offset,
52
+            )
45 53
             const queueIds = queue.map(entry => entry.target_id)
46
-            // console.log('queueIds', queueIds)
47
-            const res = {
48
-                ok: true,
49
-                handler: pluginConfig.handlerType,
50
-                data: queueIds,
51
-            }
52 54
 
53 55
             // HELP: I think there's an issue here
54 56
             // queueIds spits out the queue profiles in the correct order
@@ -58,15 +60,17 @@ module.exports = {
58 60
             //     'include_profile results',
59 61
             //     await profileService.getProfilesFor(queueIds),
60 62
             // )
61
-            if (include_profile) {
62
-                res.data = await profileService.getProfilesFor(
63
-                    queueIds,
64
-                    'participant',
65
-                    false,
66
-                )
67
-            }
68 63
             try {
69
-                return h.response(res).code(200)
64
+                return h
65
+                    .response({
66
+                        ok: true,
67
+                        handler: pluginConfig.handlerType,
68
+                        data: await profileService.getProfilesFor(
69
+                            queueIds,
70
+                            'participant',
71
+                        ),
72
+                    })
73
+                    .code(200)
70 74
             } catch (err) {
71 75
                 return h
72 76
                     .response({

+ 6
- 0
backend/lib/routes/profile/score.js Ver arquivo

@@ -53,11 +53,17 @@ module.exports = {
53 53
             const distanceUnit = request.query.unit
54 54
                 ? request.query.unit
55 55
                 : 'mile'
56
+            const duration = request.query.duration
57
+            const presence = request.query.presence
58
+            const certifications = request.query.certifications
56 59
 
57 60
             const scoredProfiles = await profileService.scoreProfilesFor(
58 61
                 profileId,
59 62
                 maxDistanceMiles,
60 63
                 distanceUnit,
64
+                duration,
65
+                presence,
66
+                certifications,
61 67
             )
62 68
             try {
63 69
                 if (!scoredProfiles) {

+ 5
- 1
backend/lib/routes/user/create-profile.js Ver arquivo

@@ -67,10 +67,14 @@ module.exports = {
67 67
                 }
68 68
                 /** Grab payload info */
69 69
                 const res = request.payload
70
+                /** Don't log password in response table */
71
+                const resWithoutPass = res.filter(r => {
72
+                    return r.response_key_id !== 9
73
+                })
70 74
                 const profile =
71 75
                     await profileService.saveResponsesCreateProfileFor(
72 76
                         userId,
73
-                        res,
77
+                        resWithoutPass,
74 78
                     )
75 79
                 return h
76 80
                     .response({

+ 72
- 0
backend/lib/routes/user/email.js Ver arquivo

@@ -0,0 +1,72 @@
1
+'use strict'
2
+
3
+const Joi = require('joi')
4
+
5
+const pluginConfig = {
6
+    handlerType: 'email',
7
+    docs: {
8
+        get: {
9
+            description: 'sends confirmation email',
10
+            notes: 'Stores the email in memory in a hash and sends confirmation email for signup',
11
+        },
12
+    },
13
+}
14
+
15
+module.exports = {
16
+    method: 'POST',
17
+    path: '/sendemail/',
18
+    options: {
19
+        ...pluginConfig.docs.get,
20
+        tags: ['api'],
21
+        auth: false,
22
+        cors: true,
23
+        handler: async function (request, h) {
24
+            const { userService } = request.server.services()
25
+            const userCredentials = request.payload
26
+            try {
27
+                const emailSent = await userService.emailSent(userCredentials)
28
+                const hashedAccessToken = Object.keys(
29
+                    userService.activeSessions,
30
+                ).find(hashedToken => {
31
+                    return (
32
+                        userService.activeSessions[`${hashedToken}`].email ===
33
+                        userCredentials.email
34
+                    )
35
+                })
36
+                // Registers the activeSessions object for use by jwt auth strategy
37
+                request.server.app.activeSessions = userService.activeSessions
38
+                if (!hashedAccessToken.length) {
39
+                    throw Error('hashedAccessToken not Found!!')
40
+                }
41
+                return {
42
+                    ok: true,
43
+                    handler: pluginConfig.handlerType,
44
+                    data: {
45
+                        emailSentSuccessfully: emailSent.wasSuccessfull,
46
+                        hashedAccessToken: hashedAccessToken,
47
+                    },
48
+                }
49
+            } catch (err) {
50
+                console.log('err :=>', err)
51
+                return {
52
+                    ok: false,
53
+                    handler: pluginConfig.handlerType,
54
+                    data: {
55
+                        error: err,
56
+                    },
57
+                }
58
+            }
59
+        },
60
+        validate: {
61
+            failAction: 'log',
62
+        },
63
+        response: {
64
+            schema: Joi.object({
65
+                ok: Joi.bool(),
66
+                handler: Joi.string(),
67
+                data: Joi.object(),
68
+            }).label('email_res'),
69
+            failAction: 'log',
70
+        },
71
+    },
72
+}

+ 58
- 0
backend/lib/routes/user/getaccess.js Ver arquivo

@@ -0,0 +1,58 @@
1
+'use strict'
2
+
3
+const Joi = require('joi')
4
+
5
+const pluginConfig = {
6
+    handlerType: 'authentication',
7
+    docs: {
8
+        get: {
9
+            description: 'gets session token for authentication',
10
+            notes: 'Gets session token for authentication',
11
+        },
12
+    },
13
+}
14
+
15
+module.exports = {
16
+    method: 'POST',
17
+    path: '/getaccess',
18
+    options: {
19
+        ...pluginConfig.docs.get,
20
+        tags: ['api'],
21
+        auth: false,
22
+        cors: {
23
+            headers: ['Authorization', 'Content-Type'],
24
+            exposedHeaders: ['Authorization', 'Access-Control-Expose-Headers'],
25
+        },
26
+        handler: async function (request, h) {
27
+            const { userService } = request.server.services()
28
+            const res = request.payload
29
+            // NOTE: Access Token set for 5 minutes expiration (default)
30
+            const accessToken = await userService.createToken(res, 600)
31
+            try {
32
+                const response = h.response({
33
+                    ok: true,
34
+                    handler: pluginConfig.handlerType,
35
+                    data: accessToken,
36
+                })
37
+                response.header('Authorization', accessToken)
38
+                return response
39
+            } catch (err) {
40
+                return {
41
+                    ok: false,
42
+                    handler: pluginConfig.handlerType,
43
+                    data: {
44
+                        error: err,
45
+                    },
46
+                }
47
+            }
48
+        },
49
+        validate: {
50
+            failAction: 'log',
51
+        },
52
+        response: {
53
+            // TODO: change back to accommodate new h.response return values
54
+            schema: Joi.any().label('get_session_res'),
55
+            failAction: 'log',
56
+        },
57
+    },
58
+}

+ 1
- 2
backend/lib/routes/user/list-profiles.js Ver arquivo

@@ -37,8 +37,7 @@ module.exports = {
37 37
     options: {
38 38
         ...pluginConfig.docs,
39 39
         tags: ['api'],
40
-        /** Protect this route with authentication? */
41
-        auth: false,
40
+        auth: 'default_jwt',
42 41
         cors: true,
43 42
         handler: async function (request, h) {
44 43
             const { userService, profileService } = request.server.services()

+ 5
- 1
backend/lib/routes/user/signup.js Ver arquivo

@@ -30,6 +30,9 @@ const responseSchemas = {
30 30
     error: errorSchema.single,
31 31
 }
32 32
 
33
+// Brevo logic will go here,
34
+// send Brevo email with link that has jwt in it?
35
+
33 36
 module.exports = {
34 37
     method: 'POST',
35 38
     path: '/signup',
@@ -56,7 +59,7 @@ module.exports = {
56 59
                         is_admin: 0,
57 60
                         is_verified: 0,
58 61
                     },
59
-                    created_at: Date.now()
62
+                    created_at: Date.now(),
60 63
                 })
61 64
                 return h
62 65
                     .response({
@@ -66,6 +69,7 @@ module.exports = {
66 69
                     })
67 70
                     .code(201)
68 71
             } catch (err) {
72
+                console.error('ERROR :=>', err)
69 73
                 return h
70 74
                     .response({
71 75
                         ok: false,

+ 79
- 0
backend/lib/routes/user/validatesession.js Ver arquivo

@@ -0,0 +1,79 @@
1
+'use strict'
2
+
3
+const { plugin } = require('@hapi/inert')
4
+const Joi = require('joi')
5
+
6
+const pluginConfig = {
7
+    handlerType: 'jwt',
8
+    docs: {
9
+        get: {
10
+            description: 'validates session token for each step of survey',
11
+            notes: 'validates session token for each step of survey',
12
+        },
13
+    },
14
+}
15
+
16
+module.exports = {
17
+    method: 'POST',
18
+    path: '/validatesession',
19
+    options: {
20
+        ...pluginConfig.docs.get,
21
+        tags: ['api'],
22
+        auth: false,
23
+        cors: {
24
+            headers: ['Authorization', 'Content-Type'],
25
+            exposedHeaders: ['Authorization', 'Access-Control-Expose-Headers'],
26
+        },
27
+        handler: async function (request, h) {
28
+            const hashedAccessToken = request.payload
29
+            const { userService, profileService } = request.server.services()
30
+            try {
31
+                const validatedSessionToken =
32
+                    userService.validateSession(hashedAccessToken)
33
+                const user = await userService.findByUserEmail(
34
+                    validatedSessionToken.email,
35
+                )
36
+                const type = user.is_poster == 1 ? 'poster' : 'seeker'
37
+                const profiles = await profileService.getCompleteProfilesFor(
38
+                    user.user_id,
39
+                    type,
40
+                )
41
+                // TODO: handle user with multiple profiles...
42
+                const profileId = profiles[0].profile_id
43
+                const responses = []
44
+                profiles[0].responses.forEach(response => {
45
+                    responses.push({
46
+                        response_key_id: response.response_key_id,
47
+                        val: response.val,
48
+                    })
49
+                })
50
+                return {
51
+                    ok: true,
52
+                    handler: pluginConfig.handlerType,
53
+                    data: {
54
+                        ...validatedSessionToken,
55
+                        profileId: profileId,
56
+                        responses: responses,
57
+                    },
58
+                }
59
+            } catch (err) {
60
+                return {
61
+                    ok: false,
62
+                    handler: pluginConfig.handlerType,
63
+                    data: { error: err.message },
64
+                }
65
+            }
66
+        },
67
+        validate: {
68
+            failAction: 'log',
69
+        },
70
+        response: {
71
+            schema: Joi.object({
72
+                ok: Joi.bool(),
73
+                handler: Joi.string(),
74
+                data: Joi.object(),
75
+            }).label('validate_session_res'),
76
+            failAction: 'log',
77
+        },
78
+    },
79
+}

+ 78
- 0
backend/lib/routes/user/verifyactivesession.js Ver arquivo

@@ -0,0 +1,78 @@
1
+'use strict'
2
+
3
+const Joi = require('joi')
4
+
5
+const pluginConfig = {
6
+    handlerType: 'email',
7
+    docs: {
8
+        get: {
9
+            description: 'verifies confirmation email',
10
+            notes: 'Verifies the email from the stored hash',
11
+        },
12
+    },
13
+}
14
+
15
+module.exports = {
16
+    method: 'GET',
17
+    path: '/verify/{hashedSessionToken}',
18
+    options: {
19
+        ...pluginConfig.docs.get,
20
+        tags: ['api'],
21
+        auth: false,
22
+        cors: true,
23
+        handler: async function (request, h) {
24
+            const { userService } = request.server.services()
25
+            const hash = request.params.hashedSessionToken
26
+            try {
27
+                const hashToMatch = Object.keys(
28
+                    userService.activeSessions,
29
+                ).find(hashedToken => {
30
+                    return hashedToken === hash
31
+                })
32
+                if (!hashToMatch.length) {
33
+                    throw Error('hashToMatch Not Found!')
34
+                }
35
+                const now = Date.now()
36
+                const expiration = new Date(
37
+                    userService.activeSessions[`${hash}`].expiration,
38
+                )
39
+                if (now > expiration) {
40
+                    delete userService.activeSessions[hashToMatch]
41
+                    throw new Error(
42
+                        'you took to long to respond to the email...',
43
+                    )
44
+                }
45
+                if (!hashToMatch) {
46
+                    throw new Error('no record of email in cache')
47
+                }
48
+                return {
49
+                    ok: true,
50
+                    handler: pluginConfig.handlerType,
51
+                    data: {
52
+                        hashesMatch: hashToMatch === hash,
53
+                    },
54
+                }
55
+            } catch (err) {
56
+                console.log('err :=>', err)
57
+                return {
58
+                    ok: false,
59
+                    handler: pluginConfig.handlerType,
60
+                    data: {
61
+                        error: err,
62
+                    },
63
+                }
64
+            }
65
+        },
66
+        validate: {
67
+            failAction: 'log',
68
+        },
69
+        response: {
70
+            schema: Joi.object({
71
+                ok: Joi.bool(),
72
+                handler: Joi.string(),
73
+                data: Joi.object(),
74
+            }).label('verify_email_res'),
75
+            failAction: 'log',
76
+        },
77
+    },
78
+}

+ 2
- 0
backend/lib/schemas/profiles.js Ver arquivo

@@ -13,6 +13,8 @@ const singleProfile = Joi.object({
13 13
     responses: surveyResponseSchema.list,
14 14
     reveal: Joi.array().items(),
15 15
     tags: tagSchema.list,
16
+    media: Joi.any(),
17
+    blurb: Joi.any(),
16 18
     user_type: Joi.any(),
17 19
     user: userSchema.single,
18 20
     profile_description: Joi.string().allow(null, ''),

+ 5
- 0
backend/lib/schemas/responses.js Ver arquivo

@@ -14,6 +14,11 @@ const singleResponseKey = Joi.object({
14 14
     response_key_category: Joi.string().required(),
15 15
     response_key_prompt: Joi.string().required(),
16 16
     response_key_description: Joi.any(),
17
+    aspect: Joi.string().allow(null, ''),
18
+    category: Joi.string().allow(null, ''),
19
+    placeholder: Joi.string().allow(null, ''),
20
+    invalidInputPrompt: Joi.string().allow(null, ''),
21
+
17 22
 }).label('question_single')
18 23
 
19 24
 module.exports = {

+ 48
- 0
backend/lib/services/filter.js Ver arquivo

@@ -0,0 +1,48 @@
1
+const zipcoder = require ('./profile/zipcoder')
2
+
3
+const byProfileType = (profileList, userProfile) => {
4
+    const isUserOpposite = userProfile.is_poster == 1 ? 0 : 1
5
+    return profileList.filter(profile => (profile.user.is_poster == isUserOpposite))
6
+}
7
+const byNullZip = (profileList) => {
8
+    return profileList.filter(profile => {
9
+        return zipcoder.getZipCodeFromProfile(profile) ? true : false
10
+    })
11
+}
12
+const byMaxDistance = (profileList, max) => {
13
+    return profileList.filter(profile => {
14
+        const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
15
+        const adjustedMaxDistance = Math.floor(parseFloat(max) * 100)
16
+        return profileDistance <= adjustedMaxDistance
17
+    })
18
+}
19
+
20
+const byDuration = (profileList, duration) => {
21
+    return profileList.filter(profile => {
22
+        // TODO find duration 
23
+        return profile.duration == duration
24
+    })
25
+}
26
+
27
+const byPresence = (profileList, presence) => {
28
+    return profileList.filter(profile => {
29
+        // TODO find presence 
30
+        return profile.presence == presence
31
+    })
32
+}
33
+
34
+const byCertifications = (profileList, certifications) => {
35
+    return profileList.filter(profile => {
36
+        // TODO find certifications 
37
+        return profile.certifications == certifications
38
+    })
39
+}
40
+
41
+module.exports = {
42
+    byProfileType,
43
+    byNullZip,
44
+    byMaxDistance,
45
+    byDuration,
46
+    byPresence,
47
+    byCertifications,
48
+}

+ 8
- 1
backend/lib/services/matchqueue.js Ver arquivo

@@ -9,12 +9,19 @@ module.exports = class MatchQueueService extends Schmervice.Service {
9 9
      * @param {number} profileId
10 10
      * @returns {array} MatchQueue
11 11
      */
12
-    async getQueue(profileId) {
12
+    async getQueue(profileId, limit, offset=0) {
13 13
         const { MatchQueue } = this.server.models()
14
+        if(typeof limit==='undefined') {
15
+            return await MatchQueue.query()
16
+                .where('profile_id', profileId)
17
+                .where('is_deleted', 0)
18
+        }
14 19
 
15 20
         return await MatchQueue.query()
16 21
             .where('profile_id', profileId)
17 22
             .where('is_deleted', 0)
23
+            .limit(limit)
24
+            .offset(offset)
18 25
     }
19 26
     /**
20 27
      * Returns queues by profile id by user type

+ 116
- 101
backend/lib/services/profile/index.js Ver arquivo

@@ -4,7 +4,7 @@ const config = require('../../../db/data-generator/config.json')
4 4
 const profiler = require('./profiler')
5 5
 const scoring = require('./scorer')
6 6
 const zipcoder = require('./zipcoder')
7
-const tagger = require('./tagger')
7
+const filter = require('../filter')
8 8
 
9 9
 module.exports = class ProfileService extends Schmervice.Service {
10 10
     constructor(...args) {
@@ -24,15 +24,14 @@ module.exports = class ProfileService extends Schmervice.Service {
24 24
         }
25 25
     }
26 26
     async _setTagLookup() {
27
-        if (!Object.keys(this.tagLookup).length) {
28
-            const { Tag } = this.server.models()
29
-            const allTagDescriptions = await Tag.query()
30
-            allTagDescriptions.forEach(desc => {
31
-                if (desc.is_active) {
32
-                    this.tagLookup[desc.tag_id] = desc
33
-                }
34
-            })
35
-        }
27
+        /** Grab tag descriptions if they do NOT exist: Needed once per app load */
28
+        if (Object.keys(this.tagLookup).length) return
29
+        const { Tag } = this.server.models()
30
+        const allTagDescriptions = await Tag.query()
31
+        allTagDescriptions.forEach(desc => {
32
+            if (!desc.is_active) return
33
+            this.tagLookup[desc.tag_id] = desc
34
+        })
36 35
     }
37 36
     /**
38 37
      * Internal method to get list of profile_ids for this user
@@ -52,20 +51,38 @@ module.exports = class ProfileService extends Schmervice.Service {
52 51
         return [...new Set(profileIdsToGrab)]
53 52
     }
54 53
 
54
+    /**
55
+     * Convert indexes to actual score values
56
+     * Using using the input and converting to index
57
+     * of the generated possible prescore array in config
58
+     */
59
+    _convertResponse(responseToSave) {
60
+        if (scoring._isScorableResponse(responseToSave.response_key_id)) {
61
+            // Convert -3 to 0, 0 to 3, 3 to 6
62
+            const offset = (config.scoreVals.length - 1) / 2
63
+            const scoreFromInput = parseInt(responseToSave.val) + offset
64
+            const scoreFromConfig = config.scoreVals.indexOf(scoreFromInput)
65
+            if (scoreFromConfig < 0) {
66
+                console.error('score not found in possible config responses')
67
+            }
68
+            responseToSave.val = scoreFromConfig.toString()
69
+        }
70
+        return responseToSave
71
+    }
72
+
55 73
     async getProfile(profileId) {
56 74
         const { Profile } = this.server.models()
57 75
         await this._setTagLookup()
58
-
59 76
         const matchingProfile = await Profile.query()
60 77
             .where('profile_id', profileId)
61 78
             .first()
62 79
             .withGraphFetched('tags')
63 80
             .withGraphFetched('responses')
64 81
             .withGraphFetched('user')
65
-
66
-        tagger.setProfileTags(matchingProfile, matchingProfile, this.tagLookup)
67
-        const complete = new profiler.CompleteProfile(matchingProfile)
68
-        return complete
82
+        matchingProfile.tags = matchingProfile.tags.map(
83
+            tag => this.tagLookup[tag.tag_id],
84
+        )
85
+        return new profiler.CompleteProfile(matchingProfile)
69 86
     }
70 87
 
71 88
     async getCompleteProfilesFor(userId, type) {
@@ -78,23 +95,21 @@ module.exports = class ProfileService extends Schmervice.Service {
78 95
             .whereIn('profile_id', dedupedProfileIds)
79 96
             .withGraphFetched('tags')
80 97
             .withGraphFetched('responses')
81
-            // CHECKTHIS: Added this because we added user.user_name to CompleteProfile
82
-            // so without this, we get undefined user_name
83 98
             .withGraphFetched('user')
84 99
 
85
-        return profiler.makeCompleteProfilesFromProfile(
100
+        return profiler.makeCompleteFromProfileEntries(
86 101
             profilesEntries,
87 102
             type,
88 103
             this.tagLookup,
89 104
         )
90 105
     }
91 106
 
92
-    async getProfilesFor(profileIdArray, type, includeResponses = true) {
107
+    async getProfilesFor(profileIdArray, type) {
93 108
         const { Profile } = this.server.models()
94 109
         await this._setScoreLookup()
95 110
         await this._setTagLookup()
96 111
 
97
-        // profilesEntries is profiles in dataaspect_labelsbase row order
112
+        // profilesEntries is profiles in database row order
98 113
         const profilesEntries = await Profile.query()
99 114
             .whereIn('profile_id', profileIdArray)
100 115
             .withGraphFetched('tags')
@@ -104,11 +119,10 @@ module.exports = class ProfileService extends Schmervice.Service {
104 119
         // taking the info from profilesEntries
105 120
         // to repack into completeProfiles
106 121
         // in same order as profileIdArray
107
-        return profiler.makeCompleteProfiles(
122
+        return profiler.makeOrderedCompleteProfiles(
108 123
             profileIdArray,
109 124
             profilesEntries,
110 125
             type,
111
-            includeResponses,
112 126
             this.tagLookup,
113 127
         )
114 128
     }
@@ -121,35 +135,24 @@ module.exports = class ProfileService extends Schmervice.Service {
121 135
      */
122 136
     async saveResponsesCreateProfileFor(userId, responses, txn) {
123 137
         const { Profile, Response } = this.server.models()
124
-
125
-        const profile = await Profile.query(txn).insert({
126
-            user_id: userId,
127
-        })
128
-        for (const responseToSave of responses) {
129
-            /**
130
-             * Convert indexes to actual score values
131
-             * Using using the input and converting to index
132
-             * of the generated possible prescore array in config
133
-             * DUPLICATE:See saveResponseForProfile() line 343
134
-             */
135
-            let convertedResponse = responseToSave
136
-            if (scoring._isScorableResponse(responseToSave.response_key_id)) {
137
-                // Convert -3 to 0, 0 to 3, 3 to 6
138
-                const offset = (config.scoreVals.length - 1) / 2
139
-                const indexFromInput = parseInt(responseToSave.val) + offset
140
-                convertedResponse.val =
141
-                    config.scoreVals[indexFromInput].toString()
142
-            }
143
-
144
-            const responseInfo = {
145
-                profile_id: profile.id,
146
-                response_key_id: convertedResponse.response_key_id,
147
-                val: convertedResponse.val,
138
+        try {
139
+            const profile = await Profile.query(txn).insert({
140
+                user_id: userId,
141
+            })
142
+            for (const responseToSave of responses) {
143
+                const convertedResponse = this._convertResponse(responseToSave)
144
+                const responseInfo = {
145
+                    profile_id: profile.id,
146
+                    response_key_id: convertedResponse.response_key_id,
147
+                    val: convertedResponse.val,
148
+                }
149
+                await Response.query(txn).insert(responseInfo)
148 150
             }
149
-            await Response.query(txn).insert(responseInfo)
151
+            //** Work around for HAPI returning profile_id as id */
152
+            return { user_id: profile.user_id, profile_id: profile.id }
153
+        } catch (err) {
154
+            throw new Error(err)
150 155
         }
151
-        //** Work around for HAPI returning profile_id as id */
152
-        return { user_id: profile.user_id, profile_id: profile.id }
153 156
     }
154 157
 
155 158
     /** Update responses in place
@@ -179,6 +182,14 @@ module.exports = class ProfileService extends Schmervice.Service {
179 182
         })
180 183
     }
181 184
 
185
+    async insertSingleResponseForProfile(responseToSave) {
186
+        const { Response } = this.server.models()
187
+        const convertedResponse = this._convertResponse(responseToSave)
188
+        const savedResponse = await Response.query().insert(convertedResponse)
189
+        delete savedResponse.id
190
+        return savedResponse
191
+    }
192
+
182 193
     /** Add response
183 194
      * @param {Object} response to save
184 195
      * @returns {null} updated responses
@@ -205,24 +216,7 @@ module.exports = class ProfileService extends Schmervice.Service {
205 216
                 .delete()
206 217
                 .whereIn('response_key_id', alreadyAnswered)
207 218
         }
208
-
209
-        /**
210
-         * Convert indexes to actual score values
211
-         * Using using the input and converting to index
212
-         * of the generated possible prescore array in config
213
-         */
214
-        let convertedResponse = responseToSave
215
-        if (scoring._isScorableResponse(responseToSave.response_key_id)) {
216
-            // Convert -3 to 0, 0 to 3, 3 to 6
217
-            const offset = (config.scoreVals.length - 1) / 2
218
-            const scoreFromInput = parseInt(responseToSave.val) + offset
219
-            const scoreFromConfig = config.scoreVals.indexOf(scoreFromInput)
220
-            if (scoreFromConfig < 0) {
221
-                console.error('score not found in possible config responses')
222
-            }
223
-            convertedResponse.val = scoreFromConfig.toString()
224
-        }
225
-
219
+        const convertedResponse = this._convertResponse(responseToSave)
226 220
         await Response.query().insert(convertedResponse)
227 221
         return allResponses
228 222
     }
@@ -249,7 +243,14 @@ module.exports = class ProfileService extends Schmervice.Service {
249 243
      * @param {number} profileId
250 244
      * @returns {Array} Ordered and scored Profiles
251 245
      */
252
-    async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
246
+    async scoreProfilesFor(
247
+        profileId,
248
+        maxDistance,
249
+        distanceUnit,
250
+        duration,
251
+        presence,
252
+        certifications,
253
+    ) {
253 254
         const { Profile } = this.server.models()
254 255
 
255 256
         await this._setScoreLookup()
@@ -260,29 +261,41 @@ module.exports = class ProfileService extends Schmervice.Service {
260 261
             .withGraphFetched('responses')
261 262
             .withGraphFetched('user')
262 263
 
263
-        // Move unneeded responses
264 264
         const userZip = zipcoder.getZipCodeFromProfile(userProfile)
265 265
 
266
-        // Find all Profiles that are NOT of our userProfile.type
267
-        // ie. If userProfile.type == seeker, then find: poster
268
-        let profileIdsOfOppositeType = await Profile.query()
266
+        // preprocess potential match pool with filter service methods
267
+        let matchPool = await Profile.query()
269 268
             .withGraphFetched('responses')
270 269
             .withGraphFetched('user')
271
-        // TODO: Let Objection optimize this
272
-        const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
273
-        profileIdsOfOppositeType = profileIdsOfOppositeType
274
-            .filter(profile => {
275
-                return profile.user.is_poster == isPosterOpposite
276
-            })
277
-            .filter(profile => {
278
-                // Only include profiles that included zipcode response
279
-                return zipcoder.getZipCodeFromProfile(profile) ? true : false
280
-            })
281 270
 
282
-        const profilePlusDistance = await Promise.all(
283
-            profileIdsOfOppositeType.map(async profile => {
284
-                const targetZip = zipcoder.getZipCodeFromProfile(profile)
271
+        matchPool = filter.byProfileType(matchPool, userProfile.user)
272
+        matchPool = filter.byNullZip(matchPool)
273
+        // attach distance to pool profiles for max distance filter
274
+        matchPool = await this.calcProfileDistances(
275
+            matchPool,
276
+            distanceUnit,
277
+            userZip,
278
+        )
279
+        matchPool = filter.byMaxDistance(matchPool, maxDistance)
280
+        matchPool = filter.byDuration(matchPool, duration)
281
+        matchPool = filter.byPresence(matchPool, presence)
282
+        matchPool = filter.byCertifications(matchPool, certifications)
285 283
 
284
+        const scoredProfilesWithDistance = scoring.scoreAll(
285
+            matchPool,
286
+            userProfile,
287
+            this.scoreLookup,
288
+        )
289
+        // Order by score
290
+        return scoredProfilesWithDistance.sort(
291
+            (a, b) => b.score.total - a.score.total,
292
+        )
293
+    }
294
+
295
+    async calcProfileDistances(matchPool, distanceUnit, userZip) {
296
+        await Promise.all(
297
+            matchPool.map(async profile => {
298
+                const targetZip = zipcoder.getZipCodeFromProfile(profile)
286 299
                 if (!userZip || !targetZip)
287 300
                     return { ...profile, distance: [9999, distanceUnit] }
288 301
 
@@ -297,20 +310,6 @@ module.exports = class ProfileService extends Schmervice.Service {
297 310
                 }
298 311
             }),
299 312
         )
300
-
301
-        const distanceFilteredProfiles = zipcoder.filterByDistance(
302
-            profilePlusDistance,
303
-            maxDistance,
304
-        )
305
-        const scoredProfilesWithDistance = scoring.scoreAll(
306
-            distanceFilteredProfiles,
307
-            userProfile,
308
-            this.scoreLookup,
309
-        )
310
-        // Order by score
311
-        return scoredProfilesWithDistance.sort(
312
-            (a, b) => b.score.total - a.score.total,
313
-        )
314 313
     }
315 314
 
316 315
     /**
@@ -371,10 +370,26 @@ module.exports = class ProfileService extends Schmervice.Service {
371 370
                     : true
372 371
             })
373 372
     }
373
+
374
+    /**
375
+     * Use the db to grab tag associations
376
+     * by profile, grouping, tag, and insert
377
+     * it if it already exists
378
+     * @param {object} association
379
+     */
374 380
     async revealProfileInfo(association) {
375 381
         const { TagAssociation } = this.server.models()
376
-        await TagAssociation.query().insert(association)
377 382
 
378
-        return await this.getTagsFor(association.profile_id)
383
+        const existingAssociations = await TagAssociation.query()
384
+            .where('profile_id', `${association.profile_id}`)
385
+            .where('grouping_id', `${association.grouping_id}`)
386
+            .where('tag_id', `${association.tag_id}`)
387
+            .where('is_deleted', 0)
388
+        if (!existingAssociations.length) {
389
+            await TagAssociation.query().insert(association)
390
+            return await this.getTagsFor(association.profile_id)
391
+        } else {
392
+            return console.error('ERROR =>: tag association already exists')
393
+        }
379 394
     }
380 395
 }

+ 88
- 73
backend/lib/services/profile/profiler.js Ver arquivo

@@ -1,5 +1,18 @@
1 1
 const config = require('../../../db/data-generator/config.json')
2
-const tagger = require('./tagger')
2
+
3
+/**
4
+ * Unscored preferences used for filtering
5
+ * Does NOT include blurb, media languages
6
+ */
7
+const unscoredProfilePreferences = [
8
+    'zipcode',
9
+    'duration',
10
+    'presence',
11
+    'urgency',
12
+    'pronouns',
13
+    'distance',
14
+]
15
+const otherProfileInfo = ['blurb', 'media', 'lang']
3 16
 
4 17
 /**
5 18
  * Class to hold our retrieved profile information
@@ -7,96 +20,98 @@ const tagger = require('./tagger')
7 20
  * !: This needs to match the responseSchema in profiles.js
8 21
  */
9 22
 class CompleteProfile {
10
-    constructor(profile, type, includeResponses = false) {
23
+    constructor(profile, type) {
11 24
         this.user_id = profile.user_id // int user_id
12 25
         this.profile_id = profile.profile_id // int profile_id
13
-        this.user_name = profile.user.user_name // string user_name
14
-        this.user_email = profile.user.user_email
15
-        this.responses = []
26
+        this.responses = profile.responses
16 27
         this.user_type = type
17
-        this.tags = profile.tags.filter(t => t.tag_category != 'reveal')
18
-
19
-        // TODO: generalize this for multiple images, and languages
20
-        this.profile_description = ''
28
+        this.blurb = ''
21 29
         this.profile_media = []
22 30
         this.profile_languages = []
23
-        this.profile_prefs = {}
24
-
25
-        // TODO: Use reveal tags to add or remove information from profile!
26
-        // TODO: ---
27
-        // TODO: Use reveal tags to rebuild profile based on group/membership
28
-        // TODO: and include for certain profiles
31
+        this.profile_prefs = this.getPrefsFromResponses(this.responses)
29 32
 
30
-        this.reveal = profile.tags.filter(t => t.category == 'reveal')
31
-        // TODO: filter these correctly
32
-        if (profile?.responses?.length && includeResponses) {
33
-            // [] of all "profile" responses
34
-            this.responses = profile.responses
35
-            // image, language, duration, presence, blurb, urgency, role, pronouns, distance
36
-            const prefs = [
37
-                'zipcode',
38
-                'duration',
39
-                'presence',
40
-                'urgency',
41
-                'role',
42
-                'pronouns',
43
-                'distance',
44
-            ]
45
-            const prefsKeys = config.prefKeys
46
-            prefs.forEach((pref, i) => {
47
-                this.profile_prefs[pref] = this.responses.filter(
48
-                    r => r.response_key_id === prefsKeys[i],
49
-                )[0]
50
-            })
51
-            this.profile_description = this.responses
52
-                .filter(r => r.response_key_id === config.blurbKey)
53
-                .map(r => r.val)[0]
54
-            this.profile_media = this.responses
55
-                .filter(r => r.response_key_id === config.mediaKey)
56
-                .map(r => r.val)
57
-            this.profile_languages = this.responses
58
-                .filter(r => r.response_key_id === config.langKey)
59
-                .map(r => r.val)
60
-        }
33
+        otherProfileInfo.forEach(prefName => {
34
+            if (prefName == 'blurb') {
35
+                const blurbRes = this.responses.find(
36
+                    r => r.response_key_id === config.blurbKey,
37
+                )
38
+                this.profile_description = blurbRes ? blurbRes.val : ''
39
+            } else if (['media', 'lang'].includes(prefName)) {
40
+                const key =
41
+                    prefName == 'media'
42
+                        ? `profile_${prefName}`
43
+                        : [`profile_${prefName}uages`]
44
+                const resForKey = this.responses.filter(
45
+                    r => r.response_key_id === config[`${prefName}Key`],
46
+                )
47
+                this[key] = resForKey.length ? resForKey.map(r => r.val) : []
48
+            }
49
+        })
50
+        // TODO: These should be getters
51
+        this.user_name = 'bleh'
52
+        this.user_email = 'bleh@bleh.com'
53
+        this.reveal = profile.tags.filter(t => t.tag_category == 'reveal')
54
+        this.tags = profile.tags.filter(t => t.tag_category !== 'reveal')
55
+    }
56
+    /** Map pref name to dB key associated with preference */
57
+    get byPrefName() {
58
+        return unscoredProfilePreferences.reduce((byPref, prefName) => {
59
+            byPref[prefName] = this.responses.find(
60
+                r => config[`${prefName}Key`] == r.response_key_id,
61
+            )?.val
62
+            return byPref
63
+        }, {})
64
+    }
65
+    getPrefsFromResponses(responses) {
66
+        if (!responses.length) return
67
+        const prefs = {}
68
+        unscoredProfilePreferences.forEach(prefName => {
69
+            prefs[prefName] = this.byPrefName[prefName]
70
+        })
71
+        return prefs
61 72
     }
62 73
 }
74
+const _makeCompleteProfile = (profileEntry, type, tagLookup) => {
75
+    profileEntry.tags = profileEntry.tags.map(tag => tagLookup[tag.tag_id])
76
+    const complete = new CompleteProfile(profileEntry, type)
77
+    return complete
78
+}
63 79
 
64
-const makeCompleteProfiles = (
65
-    profileIdArray,
80
+/**
81
+ * Get complete profiles and return in order
82
+ * @param {Array} orderedProfileIds
83
+ * @param {Array} profilesEntries
84
+ * @param {String} type
85
+ * @param {Object} tagLookup
86
+ * @returns {Array}
87
+ */
88
+const makeOrderedCompleteProfiles = (
89
+    orderedProfileIds,
66 90
     profilesEntries,
67 91
     type,
68
-    includeResponses,
69 92
     tagLookup,
70 93
 ) => {
71
-    const completeProfiles = []
72
-    profileIdArray.forEach(pid => {
73
-        profilesEntries.forEach(entry => {
74
-            if (entry.profile_id == pid) {
75
-                const complete = new CompleteProfile(
76
-                    entry,
77
-                    type,
78
-                    includeResponses,
79
-                )
80
-                tagger.setProfileTags(entry, complete, tagLookup)
81
-                completeProfiles.push(complete)
82
-            }
83
-        })
94
+    return orderedProfileIds.map(pid => {
95
+        const found = profilesEntries.find(entry => entry.profile_id == pid)
96
+        return _makeCompleteProfile(found, type, tagLookup)
84 97
     })
85
-    return completeProfiles
86 98
 }
87
-const makeCompleteProfilesFromProfile = (profilesEntries, type, tagLookup) => {
88
-    profilesEntries.forEach(profile => {
89
-        tagger.setProfileTags(profile, profile, tagLookup)
90
-    })
91 99
 
92
-    //** Get responses asociated with each profile_id */
93
-    return profilesEntries.map(profile => {
94
-        return new CompleteProfile(profile, type)
95
-    })
100
+/**
101
+ * Get complete profiles from dB rows
102
+ * @param {Array} profilesEntries
103
+ * @param {String} type
104
+ * @param {Object} tagLookup
105
+ * @returns {Array}
106
+ */
107
+const makeCompleteFromProfileEntries = (profilesEntries, type, tagLookup) => {
108
+    return profilesEntries.map(entry =>
109
+        _makeCompleteProfile(entry, type, tagLookup),
110
+    )
96 111
 }
97 112
 
98 113
 module.exports = {
99 114
     CompleteProfile,
100
-    makeCompleteProfiles,
101
-    makeCompleteProfilesFromProfile,
115
+    makeOrderedCompleteProfiles,
116
+    makeCompleteFromProfileEntries,
102 117
 }

+ 2
- 7
backend/lib/services/profile/scorer.js Ver arquivo

@@ -18,13 +18,8 @@ const makeScoreLookup = (aspects, labels) => {
18 18
     return scoreLookup
19 19
 }
20 20
 
21
-const _isScorableResponse = res_key_id => {
22
-    let isScorable = false
23
-    if (config.resKeys.includes(res_key_id)) {
24
-        isScorable = true
25
-    }
26
-    return isScorable
27
-}
21
+const _isScorableResponse = res_key_id =>
22
+    config.scoreKeys.includes(res_key_id) ? true : false
28 23
 
29 24
 const scoreResponses = (seeker, potentialMatch, prescoreLookup) => {
30 25
     if (seeker.responses.length != potentialMatch.responses.length)

+ 0
- 7
backend/lib/services/profile/tagger.js Ver arquivo

@@ -1,7 +0,0 @@
1
-const setProfileTags = (inProfile, outProfile, tagLookup) => {
2
-    outProfile.tags = inProfile.tags.map(tag => tagLookup[tag.tag_id])
3
-}
4
-
5
-module.exports = {
6
-    setProfileTags,
7
-}

+ 0
- 9
backend/lib/services/profile/zipcoder.js Ver arquivo

@@ -11,15 +11,6 @@ const getZipCodeFromProfile = profile => {
11 11
     return zipRes.val
12 12
 }
13 13
 
14
-const filterByDistance = (profileList, max) => {
15
-    return profileList.filter(profile => {
16
-        const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
17
-        const adjustedMaxDistance = Math.floor(parseFloat(max) * 100)
18
-        return profileDistance <= adjustedMaxDistance
19
-    })
20
-}
21
-
22 14
 module.exports = {
23 15
     getZipCodeFromProfile,
24
-    filterByDistance,
25 16
 }

+ 140
- 19
backend/lib/services/user.js Ver arquivo

@@ -1,10 +1,29 @@
1 1
 'use strict'
2 2
 require('dotenv').config()
3
+const crypto = require('crypto')
3 4
 const Util = require('util')
4
-const Jwt = require('@hapi/jwt')
5
+const JWT = require('jsonwebtoken')
5 6
 const Schmervice = require('@hapipal/schmervice')
6 7
 const SecurePassword = require('secure-password')
7 8
 
9
+// Configuration for Brevo
10
+const SibApiV3Sdk = require('sib-api-v3-sdk')
11
+const { access, accessSync } = require('fs')
12
+const defaultClient = SibApiV3Sdk.ApiClient.instance
13
+const apiKey = defaultClient.authentications['api-key']
14
+apiKey.apiKey = process.env.BREVO_KEY
15
+
16
+const apiInstance = new SibApiV3Sdk.TransactionalEmailsApi()
17
+
18
+const hashToken = async token => {
19
+    const salt = process.env.APP_SESSION_SALT
20
+    try {
21
+        return crypto.createHmac('sha256', salt).update(token).digest('hex')
22
+    } catch (err) {
23
+        throw new Error(err.message)
24
+    }
25
+}
26
+
8 27
 const hasher = async (pwd, steak) => {
9 28
     const hash = await pwd.hash(steak)
10 29
     const result = await pwd.verify(steak, hash)
@@ -24,7 +43,8 @@ const hasher = async (pwd, steak) => {
24 43
             try {
25 44
                 squirtle = await pwd.hash(steak)
26 45
                 // console.log('improvedHash', squirtle)
27
-                // const saveHash = Auth.insert({user_email: matchingEmails}, ).into('token')
46
+                // const saveHash = Auth.insert({user_email:
47
+                // matchingEmails}).into('token')
28 48
                 return squirtle
29 49
             } catch (err) {
30 50
                 console.error(
@@ -44,6 +64,27 @@ module.exports = class UserService extends Schmervice.Service {
44 64
     constructor(...args) {
45 65
         super(...args)
46 66
         const pwd = new SecurePassword()
67
+        // TODO: Invalidate this application state somehow after a
68
+        // certain time period has passed
69
+        this.activeSessions = {
70
+            // abc123456: '123456689',
71
+            // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: {
72
+            // email: rawEmailString,
73
+            // name: 'Joe Doe',
74
+            // seeking: 'candidate'
75
+            // sessionToken: rawSessionToken, // use for expires instead of expires?
76
+            // expires: expirationTime in seconds
77
+            // }
78
+        }
79
+        // Check the hashedCookie which is our hashedSessionToken string
80
+        // validate whether or not the rawAccessToken is still valid, if valid good to go.
81
+        // if NOT valid, then we need to reassign accessToken to a newAccessToken
82
+        // this.activeSessions = {
83
+        // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: {
84
+        // accessToken: 'as;dflkja;;dlfkja;sldkf... rawAccessToken'
85
+        // }
86
+        // }
87
+
47 88
         this.pwd = {
48 89
             hash: Util.promisify(pwd.hash.bind(pwd)),
49 90
             verify: Util.promisify(pwd.verify.bind(pwd)),
@@ -80,6 +121,21 @@ module.exports = class UserService extends Schmervice.Service {
80 121
             .where({ user_name: username })
81 122
     }
82 123
 
124
+    /**
125
+     * Use to find first user with useremail
126
+     * @param {*} username
127
+     * @param {*} txn
128
+     * @returns
129
+     */
130
+    async findByUserEmail(userEmail, txn) {
131
+        const { User } = this.server.models()
132
+        const user = await User.query(txn)
133
+            .throwIfNotFound()
134
+            .first()
135
+            .where({ user_email: userEmail })
136
+        return user
137
+    }
138
+
83 139
     /**
84 140
      * Signup function
85 141
      * @param {*} param0
@@ -168,28 +224,49 @@ module.exports = class UserService extends Schmervice.Service {
168 224
 
169 225
     /**
170 226
      * Create a token to be sent in request headers
171
-     * @param {User} user
227
+     * @param {data, expiration}
172 228
      * @returns {Token}
173 229
      */
174
-    createToken(user) {
230
+    createToken(data, expiration = 600) {
175 231
         const key = this.server.registrations['main-app-plugin'].options.jwtKey
176
-
177
-        return Jwt.token.generate(
178
-            {
179
-                aud: 'urn:audience:test',
180
-                iss: 'urn:issuer:test',
181
-                email: user.user_email,
182
-            },
183
-            {
184
-                key: key,
185
-                algorithm: 'HS256',
186
-            },
187
-            {
188
-                ttlSec: 4 * 60 * 60, // 7 days
189
-            },
190
-        )
232
+        const obj = {}
233
+        Object.assign(obj, { ...data })
234
+        return JWT.sign(obj, key, { expiresIn: expiration })
191 235
     }
192 236
 
237
+    /**
238
+     * Validates whether a token has expired or not
239
+     * @param {User} user
240
+     * @returns {Token}
241
+     */
242
+    validateToken(token) {
243
+        const key = this.server.registrations['main-app-plugin'].options.jwtKey
244
+        try {
245
+            return JWT.verify(token, key)
246
+        } catch (err) {
247
+            return { payload: null, message: err.message }
248
+        }
249
+    }
250
+    /**
251
+     * Uses this.validateToken() to verify hashedSessionToken's
252
+     * existence, expiry, and also valdiates accessToken
253
+     * @param {HashedSessionToken} hashedSessionToken
254
+     * @returns {PayloadFromActiveSessions}
255
+     */
256
+    validateSession(hashedAccessToken) {
257
+        const userSession = this.activeSessions[hashedAccessToken]
258
+        if (!userSession) {
259
+            throw new Error(
260
+                'hashedSessionToken not in activeSessions registry!',
261
+            )
262
+        }
263
+        const accessToken = userSession.accessToken
264
+        const accessTokenIsValid = this.validateToken(accessToken)
265
+        return {
266
+            ...accessTokenIsValid.payload,
267
+            accessToken: this.activeSessions[hashedAccessToken].accessToken,
268
+        }
269
+    }
193 270
     /**
194 271
      * Use knex to try to change password entry
195 272
      * @param {number} id
@@ -223,4 +300,48 @@ module.exports = class UserService extends Schmervice.Service {
223 300
 
224 301
         return passwordRow ? passwordRow.token : null
225 302
     }
303
+
304
+    /**
305
+     * Sends a Transactional Email via Brevo
306
+     * @ returns {Object}
307
+     */
308
+    async emailSent(userCredentials) {
309
+        const hashedAccessToken = await hashToken(userCredentials.accessToken)
310
+        if (Object.keys(this.activeSessions).includes(hashedAccessToken)) {
311
+            return new Error('session already in cache!!')
312
+        }
313
+        // Set expiration time for ten minutes from now
314
+        const duration = 600000
315
+
316
+        this.activeSessions[hashedAccessToken] = {
317
+            email: userCredentials.email,
318
+            name: userCredentials.name,
319
+            seeking: userCredentials.seeking,
320
+            accessToken: userCredentials.accessToken,
321
+            expiration: Date.now() + duration,
322
+            sessionToken: null,
323
+        }
324
+
325
+        const sendSmtpEmail = {
326
+            to: [
327
+                {
328
+                    email: userCredentials.email,
329
+                },
330
+            ],
331
+            templateId: 1,
332
+            params: {
333
+                // TODO: Change this in production...
334
+                link: `localhost:3000/verify/${hashedAccessToken}`,
335
+            },
336
+        }
337
+
338
+        return await apiInstance.sendTransacEmail(sendSmtpEmail).then(
339
+            data => {
340
+                return { wasSuccessfull: true, data: data }
341
+            },
342
+            error => {
343
+                return { wasSuccessfull: false, error: error }
344
+            },
345
+        )
346
+    }
226 347
 }

+ 4631
- 2757
backend/package-lock.json
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 4
- 2
backend/package.json Ver arquivo

@@ -20,7 +20,6 @@
20 20
         "@hapi/glue": "^8.0.0",
21 21
         "@hapi/hapi": "^20.1.3",
22 22
         "@hapi/inert": "^6.0.3",
23
-        "@hapi/jwt": "^2.0.1",
24 23
         "@hapi/vision": "^6.0.1",
25 24
         "@hapipal/confidence": "^6.0.1",
26 25
         "@hapipal/schmervice": "^2.0.0",
@@ -29,13 +28,16 @@
29 28
         "compute-cosine-similarity": "^1.0.0",
30 29
         "dotenv": "^10.0.0",
31 30
         "exiting": "^6.0.1",
31
+        "hapi-auth-jwt2": "^10.4.0",
32 32
         "hapi-swagger": "^14.5.5",
33 33
         "haversine": "^1.1.1",
34 34
         "joi": "^17.4.0",
35
+        "jsonwebtoken": "^9.0.0",
35 36
         "knex": "^0.21.19",
36 37
         "mysql": "^2.18.1",
37 38
         "objection": "^2.2.18",
38
-        "secure-password": "^4.0.0"
39
+        "secure-password": "^4.0.0",
40
+        "sib-api-v3-sdk": "^8.5.0"
39 41
     },
40 42
     "devDependencies": {
41 43
         "ava": "^3.15.0",

+ 1
- 0
backend/server/index.js Ver arquivo

@@ -1,3 +1,4 @@
1
+require('dotenv').config()
1 2
 const Glue = require('@hapi/glue')
2 3
 const Exiting = require('exiting')
3 4
 const Manifest = require('./manifest')

+ 0
- 1
backend/server/manifest.js Ver arquivo

@@ -1,4 +1,3 @@
1
-require('dotenv').config()
2 1
 const Confidence = require('@hapipal/confidence')
3 2
 const Inert = require('@hapi/inert')
4 3
 const Vision = require('@hapi/vision')

BIN
frontend/assets/fonts/icomoon.eot Ver arquivo


+ 42
- 0
frontend/assets/fonts/icomoon.svg Ver arquivo

@@ -0,0 +1,42 @@
1
+<?xml version="1.0" standalone="no"?>
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
3
+<svg xmlns="http://www.w3.org/2000/svg">
4
+<metadata>Generated by IcoMoon</metadata>
5
+<defs>
6
+<font id="icomoon" horiz-adv-x="1024">
7
+<font-face units-per-em="1024" ascent="960" descent="-64" />
8
+<missing-glyph horiz-adv-x="1024" />
9
+<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
10
+<glyph unicode="&#xe900;" glyph-name="address" d="M971.947 674.987l-111.36 85.76c-7.68 4.693-15.787 8.533-24.32 11.093-8.533 2.987-17.493 4.267-26.453 4.693h-320.853l37.547-234.667h283.307c8.96 0 17.92 1.707 26.453 4.693 8.533 2.56 17.067 6.4 24.32 11.093l111.36 86.187c8.533 4.693 11.947 15.36 7.253 23.893-1.707 2.987-4.267 5.547-7.253 7.253v0zM441.6 870.4h-46.933c-12.8 0-23.467-10.667-23.467-23.467v0-164.267h-156.587c-8.96 0-17.92-1.707-26.453-4.693-8.533-2.56-17.067-6.4-24.32-11.093l-111.36-85.76c-8.533-4.693-11.947-15.36-7.253-23.893 1.707-2.987 4.267-5.547 7.253-7.253l111.36-86.187c7.68-4.693 15.787-8.533 24.32-11.093 8.533-2.987 17.493-4.267 26.453-4.693h156.587v-398.933c0-12.8 10.667-23.467 23.467-23.467h46.933c12.8 0 23.467 10.667 23.467 23.467v797.867c0 12.8-10.667 23.467-23.467 23.467v0z" />
11
+<glyph unicode="&#xe901;" glyph-name="area-graph" d="M1024 915.627v-935.68h-1007.787c-16.213 0-21.333 12.373-11.093 27.307l228.693 336.213c8.96 13.653 25.6 16.64 37.547 6.4 0.853-0.853 1.707-1.707 2.56-2.56l72.533-78.080c9.387-11.093 24.747-11.093 34.133 0 1.707 1.707 2.987 3.84 3.84 5.973l156.16 277.333c6.4 13.227 21.333 17.493 32.427 10.24 2.133-1.28 3.84-2.987 5.12-4.693l111.36-122.453c8.533-10.667 23.040-11.093 32.427-0.853 1.707 2.133 3.413 4.267 4.693 6.827l263.68 453.973c6.827 13.653 20.053 21.333 33.707 20.053v0z" />
12
+<glyph unicode="&#xe902;" glyph-name="calendar" d="M910.080 846.080h-56.747v-113.92h-170.667v113.92h-341.333v-113.92h-170.667v113.92h-56.747c-62.72 0-113.92-50.773-113.92-113.493v-682.667c0-62.72 51.2-113.493 113.92-113.92h796.16c62.72 0 113.493 51.2 113.92 113.92v682.667c0 62.72-51.2 113.493-113.92 113.92zM910.080 49.493h-796.16v455.253h796.16v-454.827zM312.747 960h-113.92v-199.253h113.92v199.253zM824.747 960h-113.92v-199.253h113.92v199.253z" />
13
+<glyph unicode="&#xe903;" glyph-name="certified" d="M509.013-64c-13.653 1.28-26.027 8.96-32.427 21.333l-54.187 82.347c-13.227 19.2-34.987 30.72-58.027 30.293-11.52 0-23.040-2.987-33.28-8.96l-130.133-75.947c-6.827-4.693-14.933-7.253-23.467-8.107-4.693 0-9.387 1.707-12.373 5.547-4.693 10.24-4.693 21.76 0 32l56.747 173.653c5.547 15.36 3.84 32.427-4.693 46.507-8.533 13.227-23.040 22.187-38.827 23.467l-110.933 13.653c-16.64 2.133-27.733 8.533-30.72 18.347s2.56 21.333 15.36 32.853l91.307 81.92c12.373 10.667 19.627 26.453 19.627 43.093s-7.253 32.427-19.627 43.093l-91.307 81.92c-11.52 8.107-17.493 21.76-15.787 35.413 4.693 13.227 15.787 22.613 29.44 24.32l81.493 22.187c35.413 11.52 59.307 44.8 58.453 82.347l-4.693 86.187c-1.28 12.373 2.133 24.747 10.24 34.133 5.973 6.4 14.080 9.813 23.040 9.387 7.68 0 15.36-2.133 22.187-5.547l84.48-39.253c8.96-3.84 18.347-5.973 28.16-5.973 25.6 0 49.493 13.653 62.72 35.84l48.213 84.48c5.547 12.8 17.92 21.76 32 23.040 14.080-1.28 26.453-10.667 32-23.467l51.627-93.013c10.667-19.627 31.147-32 53.76-32 11.52 0 22.613 3.413 32.427 9.387l142.933 89.173c6.827 5.12 14.933 8.107 23.467 8.96 4.267 0 8.107-1.707 10.667-5.12 4.267-10.24 3.84-21.333-1.28-31.147l-64.427-180.48c-5.973-14.933-4.693-31.573 3.413-45.227 8.533-13.227 22.613-21.333 38.4-22.187l121.173-11.52c17.067-1.707 27.733-7.68 30.72-17.067s-2.56-20.907-15.36-32.427l-91.307-81.92c-12.373-10.667-19.627-26.453-19.627-43.093s7.253-32.427 19.627-43.093l91.307-81.92c11.52-8.107 17.493-21.76 15.787-35.413-4.693-13.227-15.787-22.613-29.44-24.32l-81.493-22.187c-35.413-11.52-59.307-44.8-58.453-81.92l4.693-86.187c1.28-11.947-2.133-24.32-9.813-33.707-5.547-5.973-13.227-8.96-21.333-8.96s-15.787 2.133-23.040 5.973l-91.733 46.080c-8.96 4.693-18.773 6.827-28.587 6.827-24.747 0-47.36-13.653-58.453-35.84l-47.36-92.587c-5.547-14.080-18.347-23.467-32.853-25.173zM336.213 465.067c-10.24 0-20.053-3.413-28.16-9.387-9.813-7.68-16.213-18.773-17.92-30.72-1.707-12.373 1.707-24.747 8.96-34.56l119.040-158.72c8.533-11.52 22.613-18.347 37.12-18.773h2.133c15.36 0.853 29.44 8.96 37.12 22.187l232.533 376.747c6.4 10.667 8.533 23.467 5.973 35.413s-10.24 22.613-20.48 29.013c-7.253 4.693-15.787 7.253-24.747 7.253-16.213 0-31.147-8.533-39.253-22.187l-196.693-318.72-78.507 104.107c-8.533 11.947-22.187 18.773-37.12 18.773v0z" />
14
+<glyph unicode="&#xe904;" glyph-name="chat" d="M304.64 332.373v325.547h-199.68c-58.027 0-104.96-46.933-104.96-104.96v-314.88c0-58.027 46.933-104.96 104.96-104.96h52.48v-157.44l157.44 157.44h262.4c58.027 0 104.96 46.933 104.96 104.96v95.573c-3.413-0.853-6.827-1.28-10.667-1.28h-366.933zM919.040 920.747h-472.747c-58.027 0-104.96-46.933-104.96-104.96v-420.267h367.36l157.44-157.44v157.44h52.48c58.027 0 104.96 46.933 104.96 104.96v314.88c0 58.027-46.933 104.96-104.96 104.96z" />
15
+<glyph unicode="&#xe905;" glyph-name="checkmark" d="M864 848.213l-480-480.427-224 224.427-160-160.427 384-384 640 640-160 160z" />
16
+<glyph unicode="&#xe906;" glyph-name="clock" d="M512 960c-282.88 0-512-229.12-512-512s229.12-512 512-512 512 229.12 512 512-229.12 512-512 512zM512 42.667c-224 0-405.333 181.333-405.333 405.333s181.76 405.333 405.333 405.333c224 0 405.333-181.333 405.333-405.333s-181.333-405.333-405.333-405.333v0zM565.333 483.84v284.16h-106.667v-333.227l-189.013-109.227 53.333-92.16 219.307 126.72c13.653 8.96 22.187 23.893 23.040 39.68v8.96l226.133 222.293c-10.24 14.507-22.187 28.16-34.56 40.96l-191.573-188.16z" />
17
+<glyph unicode="&#xe907;" glyph-name="cog" d="M917.333 448c2.987 62.72 40.107 118.613 96.427 145.92-10.24 34.56-24.32 68.267-41.387 99.84-59.733-14.507-122.453 5.973-162.133 52.907-43.947 41.387-59.733 104.533-40.96 162.133-32 17.067-65.28 31.147-99.84 41.387-32.427-57.6-92.16-93.867-157.867-96.427-65.707 2.56-125.867 38.827-157.867 96.427-34.56-10.24-68.267-24.32-99.84-41.387 18.773-57.173 2.987-120.32-40.96-162.133-39.68-46.933-102.4-67.413-162.133-52.907-17.067-32-31.147-65.28-41.387-99.84 56.32-27.307 93.44-83.2 96.427-145.92-2.56-65.707-38.827-125.44-96.427-157.867 10.24-34.56 24.32-68.267 41.387-99.84 57.173 18.773 120.32 2.987 162.133-40.96 43.947-41.387 59.733-104.533 40.96-162.133 32-17.067 65.28-31.147 99.84-41.387 32 57.6 92.16 94.293 157.867 96.427 65.707-2.56 125.867-39.253 157.867-96.427 34.56 10.24 68.267 24.32 99.84 41.387-18.773 57.6-2.987 120.32 40.96 162.133 39.68 46.933 102.4 67.413 161.707 52.907 17.067 32 31.147 65.28 41.387 99.84-56.747 27.307-93.867 83.2-96.853 145.92v0zM512 229.973c-120.747 0-218.453 97.707-218.453 218.453s97.707 218.453 218.453 218.453c120.747 0 218.453-97.707 218.453-218.453v0c0-120.747-97.707-218.453-218.453-218.453z" />
18
+<glyph unicode="&#xe908;" glyph-name="compass" d="M269.653 205.653s243.627 33.28 347.733 137.387 137.387 347.733 137.387 347.733c0 0-243.627-33.28-347.733-137.387s-137.387-347.733-137.387-347.733zM459.52 500.907c43.52 43.52 127.147 74.667 198.827 93.867-19.2-71.68-50.347-155.733-93.867-198.827-29.013-29.013-75.947-29.013-104.96 0s-29.013 75.947 0 104.96zM512 960c-282.88 0-512-229.12-512-512s229.12-512 512-512c282.88 0 512 229.12 512 512v0c0 282.88-229.12 512-512 512zM512 42.667c-224 0-405.333 181.333-405.333 405.333s181.333 405.333 405.333 405.333c224 0 405.333-181.333 405.333-405.333s-181.333-405.333-405.333-405.333v0z" />
19
+<glyph unicode="&#xe909;" glyph-name="emoji-happy" d="M512 917.333c-259.413 0-469.333-209.92-469.333-469.333s209.92-469.333 469.333-469.333 469.333 209.92 469.333 469.333c0 0 0 0 0 0 0 259.413-209.92 469.333-469.333 469.333zM512 76.373c-205.227 0-371.627 166.4-371.627 371.627s166.4 371.627 371.627 371.627c205.227 0 371.627-166.4 371.627-371.627s-166.4-371.627-371.627-371.627v0zM389.973 459.947c43.52 3.84 76.373 41.813 73.387 85.76 2.987 43.947-29.44 81.92-73.387 85.76-43.52-3.84-76.373-41.813-73.387-85.333-2.987-43.947 29.44-81.92 73.387-85.76zM634.453 459.947c43.947 3.413 76.373 41.813 73.387 85.76 2.987 43.947-29.867 81.92-73.387 85.76-43.947-3.84-76.373-41.813-73.387-85.333-2.987-43.947 29.44-81.92 73.387-85.76v0zM724.48 382.293c-17.92 8.96-39.68 2.133-49.067-15.787-35.84-53.76-98.56-83.627-162.987-77.653-64.427-5.973-126.72 23.893-162.987 77.653-9.387 17.92-32 24.747-49.493 14.933-17.493-9.387-24.32-30.293-15.787-48.213 48.213-78.933 136.107-124.587 228.267-118.187 92.16-6.4 180.48 39.253 228.267 118.187 8.96 17.92 1.707 39.68-16.213 48.64z" />
20
+<glyph unicode="&#xe90a;" glyph-name="envelope" d="M928 799.573h-832c-52.907 0-96-42.667-96-96v-576c0-52.907 43.093-96 96-96h832c52.907 0 96 43.093 96 96v576c0 52.907-43.093 96-96 96zM87.893 96l-23.467 23.467 264.107 264.107 23.467-23.467-264.107-264.107zM936.533 96l-264.107 264.107 23.467 23.467 264.107-264.107-23.467-23.467zM544.427 309.333v-21.76h-64v21.76l-416.427 385.707 40.96 40.96 407.040-378.027 407.040 377.6 40.96-40.96-416-385.28z" />
21
+<glyph unicode="&#xe90b;" glyph-name="graduation-cap" d="M197.547 391.253c11.093-78.507 66.133-143.36 141.653-167.253 92.587-40.533 136.107-84.48 172.373-84.48s77.227 38.4 169.813 78.933 67.84 52.907 89.6 140.8l-259.413-125.867-314.453 157.867zM958.72 621.227l-391.253 218.88c-34.987 17.067-76.373 17.067-111.36 0l-390.4-218.88c-30.72-17.067-30.72-45.227 0-62.293l390.827-218.88c34.987-17.067 76.373-17.067 111.36 0l252.587 141.227-273.92 64c-11.093-2.56-22.613-4.267-34.133-4.267-49.493 0-89.173 23.893-89.173 52.907s40.107 53.333 89.173 53.333c31.573 2.56 62.72-10.24 83.627-34.56l290.133-95.147 72.533 40.533c30.72 17.067 30.72 45.227 0 62.293v0zM839.253 206.507c-2.987-17.92 60.16-47.787 66.133 5.12 27.307 238.080-19.627 306.347-19.627 306.347l-65.28-36.693s55.467-52.907 18.773-274.773v0z" />
22
+<glyph unicode="&#xe90c;" glyph-name="guage" d="M416 271.787c-29.867-42.667-19.627-101.547 23.040-131.413 4.693-2.987 9.387-5.973 14.507-8.107 42.667-29.867 101.547-19.627 131.413 23.040 3.413 4.693 5.973 9.387 8.533 14.507 35.413 61.44 258.133 607.573 238.933 618.667s-381.013-454.827-416.427-516.267zM512 660.907c22.187 0 43.947-1.707 65.707-5.547 22.613 27.733 47.36 58.88 72.107 88.32-44.8 12.8-90.88 19.627-137.387 19.627-287.573 0-512.427-242.347-512.427-551.253 0-19.2 0.853-37.973 2.56-56.747 2.56-28.16 27.307-49.067 55.467-46.507s49.067 27.307 46.507 55.467v0c-1.28 15.36-2.133 31.573-2.133 47.36 0 251.733 180.053 448.853 409.6 448.853v0zM881.493 596.907c-14.080-38.4-29.44-77.653-42.667-111.36 54.613-80.64 83.627-176.213 82.773-273.493 0-16.213-0.853-32.427-2.133-48.213-2.56-28.16 17.92-53.333 46.080-55.893 0 0 0 0 0 0h4.693c26.453 0 48.64 20.053 50.773 46.507 1.28 19.2 2.56 38.4 2.56 57.6 1.707 141.653-49.067 278.613-142.507 384.853v0z" />
23
+<glyph unicode="&#xe90d;" glyph-name="heart" d="M939.947 796.16c-102.827 91.307-257.707 91.307-360.533 0l-67.413-61.867-67.84 61.867c-102.4 91.307-257.28 91.307-360.107 0-102.827-90.453-112.64-247.040-22.187-349.867 6.827-7.68 14.507-15.36 22.187-22.187l427.947-392.96 427.947 392.96c102.827 90.453 112.64 247.040 22.187 349.867-6.827 7.68-14.080 14.933-22.187 22.187z" />
24
+<glyph unicode="&#xe90e;" glyph-name="home" d="M1000.96 389.12h-94.293v-338.347c2.987-28.16-17.493-53.333-45.653-56.32-3.413 0-7.253 0-10.667 0h-225.707v338.347h-225.707v-338.347h-225.28c-28.16-2.987-53.333 17.493-56.32 45.653 0 3.413 0 7.253 0 10.667v338.347h-94.293c-33.707 0-26.453 18.347-3.413 42.24l452.267 452.693c20.053 22.187 54.613 23.467 76.373 3.413 1.28-1.28 2.133-2.133 3.413-3.413l452.267-452.693c23.040-23.893 30.293-42.24-3.413-42.24v0z" />
25
+<glyph unicode="&#xe90f;" glyph-name="location" d="M991.147 33.28l-69.973 209.92h-76.8l42.667-204.8h-750.507l42.667 204.8h-76.8l-69.973-209.92c-15.36-34.987 0.427-75.947 35.84-91.733 10.667-4.693 22.613-6.827 34.56-5.547h818.773c37.973-3.413 72.107 24.32 75.52 62.72 1.28 11.947-0.853 23.893-5.547 34.56v0zM767.573 704c0.427 141.227-114.347 256-255.573 256s-256-114.773-256-256c0-244.48 256-512 256-512s256 267.52 256 512zM373.333 701.013c0 76.373 61.867 138.24 138.24 138.24s138.24-61.867 138.24-138.24-61.867-138.24-138.24-138.24v0c-76.373 0-138.24 61.867-138.24 137.813 0 0 0 0 0 0v0z" />
26
+<glyph unicode="&#xe910;" glyph-name="lock" d="M841.813 561.92h-102.4v136.533c0 164.693-75.947 261.547-227.413 261.547s-227.413-96.853-227.413-261.547v-136.533h-113.92c-33.28-5.547-57.6-34.56-56.747-68.267v-443.733c1.707-33.28 22.613-62.72 54.187-74.24l68.267-22.187c36.267-10.667 73.387-16.213 110.933-17.493h329.813c37.547 0.853 75.093 6.827 110.933 17.493l67.84 22.187c31.147 11.52 52.48 40.96 54.187 74.24v443.733c-4.267 35.84-32.427 64-68.267 68.267zM625.493 561.92h-227.413v159.147c0 82.347 45.653 125.013 113.92 125.013s113.92-42.667 113.92-125.013v-159.147z" />
27
+<glyph unicode="&#xe911;" glyph-name="cross" d="M938.24-29.44c-46.080-46.080-120.747-46.080-166.827 0 0 0 0 0 0 0l-260.693 297.813-260.693-297.813c-46.933-45.227-121.6-44.373-166.827 2.56-44.373 45.653-44.373 118.613 0 164.267l271.36 309.76-271.36 310.187c-45.227 46.933-44.373 121.6 2.56 166.827 45.653 44.373 118.613 44.373 164.267 0l260.693-298.24 260.693 298.24c45.227 46.933 120.32 47.787 166.827 2.56 46.933-45.227 47.787-120.32 2.56-166.827-0.853-0.853-1.707-1.707-2.56-2.56l-271.36-309.76 271.36-309.76c46.080-46.080 46.080-120.747 0-167.253z" />
28
+<glyph unicode="&#xe912;" glyph-name="paper-plane" d="M995.84 858.027l-982.613-346.453c-15.787-5.547-19.2-19.2-0.427-26.453l211.2-84.48 125.013-50.347s603.307 442.88 611.413 448.853 17.493-5.12 11.947-11.947-438.187-473.6-438.187-473.6v0l-25.173-28.16 310.613-167.253c13.653-7.68 30.72-2.987 38.827 10.667 1.28 2.56 2.56 5.12 2.987 8.107 5.547 23.893 158.293 682.24 161.707 697.173 4.267 19.2-8.107 30.72-27.307 23.893v0zM348.16 49.493c0-13.653 7.68-17.493 18.347-7.68 14.080 12.8 158.72 142.507 158.72 142.507l-177.067 91.307v-226.133z" />
29
+<glyph unicode="&#xe913;" glyph-name="people" d="M817.92-16.213c0 114.773-111.787 173.227-221.013 220.16s-143.36 86.187-143.36 170.667c0 50.773 33.28 34.133 47.787 127.147 5.973 38.4 35.413 0.427 40.96 88.32 0 34.987-15.787 43.52-15.787 43.52s8.107 51.627 11.52 91.733c2.56 73.813-46.507 139.52-117.76 158.293-17.067 17.493-28.587 45.227 23.467 72.96-114.347 5.12-141.227-54.613-202.24-98.56-41.387-31.147-64.853-80.64-63.573-132.693 3.413-40.107 11.52-91.733 11.52-91.733s-16.213-8.533-16.213-43.52c5.547-87.893 34.987-49.92 40.96-88.32 14.507-92.587 47.787-76.373 47.787-127.147 0-84.48-10.667-113.067-119.467-159.573s-142.507-122.453-142.080-231.253c0-32.853-0.427-44.8-0.427-44.8h818.347s-0.427 11.947-0.427 44.373v0zM947.627 278.613c-58.027 23.467-81.92 51.2-81.92 105.813 0 32.853 21.333 22.187 30.72 81.92 4.267 24.747 23.040 0 26.453 57.173 0 22.613-10.24 28.16-10.24 28.16s5.12 33.707 7.253 59.307c2.987 60.587-43.947 112.213-104.533 114.773-3.84 0-7.68 0-11.52 0-60.587 3.413-112.64-43.093-116.053-103.68 0-3.84 0-7.68 0-11.52 2.133-25.6 7.253-59.307 7.253-59.307s-10.24-5.547-10.24-28.16c3.84-56.747 22.613-32.427 26.453-57.173 9.387-60.16 30.72-49.067 30.72-81.92 0-54.613-22.613-79.787-92.587-110.080-3.413-1.707-6.4-3.413-9.387-5.12 84.053-36.267 216.32-99.413 247.467-227.413h135.253v118.187c5.973 52.48-25.6 101.547-75.52 118.613v0z" />
30
+<glyph unicode="&#xe914;" glyph-name="plus" d="M921.6 448c0-37.973-3.413-68.267-40.96-68.267h-300.373v-300.373c0-37.547-30.72-40.96-68.267-40.96s-68.267 3.413-68.267 40.96v300.373h-300.373c-37.547 0-40.96 30.72-40.96 68.267s3.413 68.267 40.96 68.267h300.373v300.373c0 37.547 30.72 40.96 68.267 40.96s68.267-3.413 68.267-40.96v-300.373h300.373c37.973 0 40.96-30.72 40.96-68.267z" />
31
+<glyph unicode="&#xe915;" glyph-name="price-ribbon" d="M661.76 413.013c8.107 14.933 22.613 25.6 39.253 28.587 15.787 2.56 28.16 14.933 30.72 30.72 2.987 16.64 13.653 31.147 28.587 39.253 14.507 7.253 22.187 23.040 20.053 38.827-2.56 16.64 2.987 33.707 14.933 46.080 11.52 11.52 14.080 29.013 6.827 43.093-7.68 15.36-7.68 33.28 0 48.213 7.253 14.507 4.693 32-6.827 43.093-11.947 12.373-17.493 29.013-14.933 46.080 2.56 15.787-5.547 31.573-20.053 38.827-14.933 8.107-25.6 22.613-28.587 39.253-2.56 15.787-14.933 28.16-30.72 30.72-16.64 2.987-31.147 13.653-39.253 28.587-7.253 14.507-23.040 22.187-39.253 19.627-16.64-2.56-33.707 2.987-46.080 14.933-11.52 11.52-28.587 14.080-43.093 6.827-15.36-7.68-33.28-7.68-48.213 0-14.507 7.253-32 4.693-43.093-6.827-12.373-11.947-29.013-17.493-46.080-14.933-15.787 2.56-31.573-5.547-38.827-19.627-8.107-14.933-22.613-25.6-39.253-28.587-15.787-2.56-28.16-14.933-30.72-30.72-2.987-16.64-13.653-31.147-28.587-39.253-14.080-7.253-22.187-23.040-19.627-38.827 2.56-16.64-2.987-33.707-14.933-46.080-11.52-11.52-14.080-29.013-6.827-43.093 7.68-15.36 7.68-33.28 0-48.213-7.253-14.507-4.693-32 6.827-43.093 11.947-12.373 17.493-29.013 14.933-46.080-2.56-15.787 5.547-31.573 19.627-38.827 14.933-8.107 25.6-22.613 28.587-39.253 2.56-15.787 14.933-28.16 30.72-30.72 16.64-2.987 31.147-13.653 39.253-28.587 7.253-14.080 23.040-22.187 38.827-19.627 16.64 2.56 33.707-2.987 46.080-14.933 11.52-11.52 29.013-14.080 43.093-6.827 15.36 7.68 33.28 7.68 48.213 0 14.507-7.253 31.573-4.693 43.093 6.827 12.373 11.947 29.013 17.493 46.080 14.933 15.787-2.56 31.573 5.547 38.827 19.627v0zM509.44 480.853c-101.12 0-183.467 81.92-183.467 183.467 0 101.12 81.92 183.467 183.467 183.467s183.467-81.92 183.467-183.467v0c0-101.12-81.92-183.467-183.467-183.467zM292.267 372.48l-66.987-380.16 168.533 25.173 149.76-81.067 66.56 378.453c-109.227-32-227.413-10.667-318.293 57.6v0zM722.773 369.92c-16.64-12.373-34.56-23.040-53.333-32l-40.107-228.267 181.76 98.56-87.893 162.133z" />
32
+<glyph unicode="&#xe916;" glyph-name="star" d="M512 960l139.093-391.253h372.907l-304.213-229.547 108.8-403.2-316.16 241.493-316.16-241.493 108.8 403.2-305.067 229.547h372.907l139.093 391.253z" />
33
+<glyph unicode="&#xe917;" glyph-name="dots-three-horizontal" d="M512 570.453c-67.413 0-122.453-54.613-122.453-122.453s54.613-122.453 122.453-122.453 122.453 54.613 122.453 122.453c0 67.413-55.040 122.453-122.453 122.453zM122.453 570.453c-67.84 0-122.453-55.040-122.453-122.453s54.613-122.453 122.453-122.453 122.453 54.613 122.453 122.453v0c0 67.413-55.040 122.453-122.453 122.453zM901.547 570.453c-67.413 0-122.453-54.613-122.453-122.453 0-67.413 54.613-122.453 122.453-122.453 67.413 0 122.453 54.613 122.453 122.453v0c0 67.413-55.040 122.453-122.453 122.453z" />
34
+<glyph unicode="&#xe918;" glyph-name="tools" d="M159.147 608c47.787 37.12 87.893 11.52 140.8-49.92 5.973-6.827 14.080 1.28 18.773 5.12s74.667 66.987 78.080 69.973c4.693 2.987 5.973 8.96 2.987 13.653 0 0-0.427 0.853-0.853 1.28-5.547 6.4-25.173 32-38.4 49.067-93.013 121.173 254.293 203.947 200.96 205.227-27.307 0.853-136.107 2.133-152.32 0-66.133-6.827-148.907-68.693-190.293-97.28-28.16-18.347-54.187-38.827-78.507-62.293-15.36-13.653-2.56-44.373-30.293-69.12-29.44-26.027-48.213-6.4-65.28-21.333-8.533-7.68-32-25.173-38.827-31.147s-7.68-16.64-1.707-23.467c0 0 0-0.427 0.427-0.853 0 0 64.853-71.68 70.4-78.080 7.253-8.107 20.053-9.813 29.013-3.84 8.96 8.107 32.427 28.587 36.267 32s-2.56 44.373 18.347 60.587v0zM452.693 581.547c-4.267 5.973-12.373 7.253-18.347 3.413-0.853-0.427-1.28-1.28-2.133-1.707l-73.813-64.427c-5.973-5.547-6.4-14.507-1.28-20.48l426.24-485.12c9.813-11.52 27.307-12.8 38.4-2.56v0s49.92 41.813 49.92 41.813c11.52 10.24 12.8 27.307 2.56 38.827l-421.547 489.813zM1021.013 788.053c-3.84 25.173-17.067 20.053-23.893 9.387s-37.12-56.747-49.493-77.227c-12.8-31.573-49.067-46.507-80.213-33.28-7.253 2.987-13.653 7.253-19.2 12.373-58.88 41.813-38.4 70.827-28.16 90.453s42.24 75.093 46.507 81.92c3.84 6.827 1.28 14.933-5.12 18.773-4.267 2.56-9.387 2.56-13.653 0-18.347-8.533-129.707-52.48-145.067-116.053-15.787-64.427 13.227-122.453-43.52-179.627l-69.12-72.107 69.547-80.64 84.907 80.64c27.307 26.027 65.707 37.547 102.827 31.147 84.053-18.773 129.707 12.373 157.44 64.853 24.747 46.507 20.48 144.64 16.64 170.24v0zM138.667 85.333c-10.667-10.667-10.667-28.16 0-38.827l48.64-47.787c11.947-9.387 29.013-7.68 38.4 4.267 0 0 0 0 0 0l252.16 247.893-77.227 87.893-262.4-253.867z" />
35
+<glyph unicode="&#xe919;" glyph-name="truck" d="M1024 416l-128 256h-192v128c0 35.413-28.587 64-64 64h-576c-35.413 0-64-28.587-64-64v-512l64-64h81.067c-35.413-61.013-14.507-139.52 46.933-174.933s139.52-14.507 174.933 46.933c23.040 39.68 23.040 88.32 0 128h354.56c-35.413-61.013-14.507-139.52 46.933-174.933 61.013-35.413 139.52-14.507 174.933 46.933 23.040 39.68 23.040 88.32 0 128h81.067v192zM704 416v192h132.693l96-192h-228.693z" />
36
+<glyph unicode="&#xe91a;" glyph-name="dots-three-vertical" d="M512 570.453c-67.413 0-122.453-54.613-122.453-122.453s54.613-122.453 122.453-122.453 122.453 54.613 122.453 122.453c0 67.413-55.040 122.453-122.453 122.453zM512 715.093c67.413 0 122.453 54.613 122.453 122.453 0 67.413-54.613 122.453-122.453 122.453s-122.453-54.613-122.453-122.453v0c0-67.413 54.613-122.453 122.453-122.453zM512 180.907c-67.413 0-122.453-54.613-122.453-122.453s54.613-122.453 122.453-122.453 122.453 54.613 122.453 122.453c0 67.413-55.040 122.453-122.453 122.453z" />
37
+<glyph unicode="&#xe986;" glyph-name="search" d="M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256z" />
38
+<glyph unicode="&#xe9ce;" glyph-name="eye" d="M512 768c-223.318 0-416.882-130.042-512-320 95.118-189.958 288.682-320 512-320 223.312 0 416.876 130.042 512 320-95.116 189.958-288.688 320-512 320zM764.45 598.296c60.162-38.374 111.142-89.774 149.434-150.296-38.292-60.522-89.274-111.922-149.436-150.296-75.594-48.218-162.89-73.704-252.448-73.704-89.56 0-176.858 25.486-252.452 73.704-60.158 38.372-111.138 89.772-149.432 150.296 38.292 60.524 89.274 111.924 149.434 150.296 3.918 2.5 7.876 4.922 11.86 7.3-9.96-27.328-15.41-56.822-15.41-87.596 0-141.382 114.616-256 256-256 141.382 0 256 114.618 256 256 0 30.774-5.452 60.268-15.408 87.598 3.978-2.378 7.938-4.802 11.858-7.302v0zM512 544c0-53.020-42.98-96-96-96s-96 42.98-96 96 42.98 96 96 96 96-42.982 96-96z" />
39
+<glyph unicode="&#xe9d1;" glyph-name="eye-blocked" d="M945.942 945.942c-18.746 18.744-49.136 18.744-67.882 0l-202.164-202.164c-51.938 15.754-106.948 24.222-163.896 24.222-223.318 0-416.882-130.042-512-320 41.122-82.124 100.648-153.040 173.022-207.096l-158.962-158.962c-18.746-18.746-18.746-49.136 0-67.882 9.372-9.374 21.656-14.060 33.94-14.060s24.568 4.686 33.942 14.058l864 864c18.744 18.746 18.744 49.138 0 67.884zM416 640c42.24 0 78.082-27.294 90.92-65.196l-121.724-121.724c-37.902 12.838-65.196 48.68-65.196 90.92 0 53.020 42.98 96 96 96zM110.116 448c38.292 60.524 89.274 111.924 149.434 150.296 3.918 2.5 7.876 4.922 11.862 7.3-9.962-27.328-15.412-56.822-15.412-87.596 0-54.89 17.286-105.738 46.7-147.418l-60.924-60.924c-52.446 36.842-97.202 83.882-131.66 138.342zM768 518c0 27.166-4.256 53.334-12.102 77.898l-321.808-321.808c24.568-7.842 50.742-12.090 77.91-12.090 141.382 0 256 114.618 256 256zM830.026 670.026l-69.362-69.362c1.264-0.786 2.53-1.568 3.786-2.368 60.162-38.374 111.142-89.774 149.434-150.296-38.292-60.522-89.274-111.922-149.436-150.296-75.594-48.218-162.89-73.704-252.448-73.704-38.664 0-76.902 4.76-113.962 14.040l-76.894-76.894c59.718-21.462 123.95-33.146 190.856-33.146 223.31 0 416.876 130.042 512 320-45.022 89.916-112.118 166.396-193.974 222.026z" />
40
+<glyph unicode="&#xe9d3;" glyph-name="bookmarks" d="M256 832v-896l320 320 320-320v896zM768 960h-640v-896l64 64v768h576z" />
41
+<glyph unicode="&#xea40;" glyph-name="arrow-left2" d="M402.746 82.746l-320 320c-24.994 24.992-24.994 65.516 0 90.51l320 320c24.994 24.992 65.516 24.992 90.51 0 24.994-24.994 24.994-65.516 0-90.51l-210.746-210.746h613.49c35.346 0 64-28.654 64-64s-28.654-64-64-64h-613.49l210.746-210.746c12.496-12.496 18.744-28.876 18.744-45.254s-6.248-32.758-18.744-45.254c-24.994-24.994-65.516-24.994-90.51 0z" />
42
+</font></defs></svg>

BIN
frontend/assets/fonts/icomoon.ttf Ver arquivo


BIN
frontend/assets/fonts/icomoon.woff Ver arquivo


BIN
frontend/assets/images/woman-1-lg.jpg Ver arquivo


BIN
frontend/assets/images/woman-1-sm.jpg Ver arquivo


+ 149
- 0
frontend/assets/sass/icons.scss Ver arquivo

@@ -0,0 +1,149 @@
1
+@font-face {
2
+    font-family: 'icomoon';
3
+    src: url('assets/fonts/icomoon.eot?6rmrq3');
4
+    src: url('assets/fonts/icomoon.eot?6rmrq3#iefix') format('embedded-opentype'),
5
+        url('assets/fonts/icomoon.ttf?6rmrq3') format('truetype'),
6
+        url('assets/fonts/icomoon.woff?6rmrq3') format('woff'),
7
+        url('assets/fonts/icomoon.svg?6rmrq3#icomoon') format('svg');
8
+    font-weight: normal;
9
+    font-style: normal;
10
+    font-display: block;
11
+}
12
+
13
+[class^="icon-"],
14
+[class*=" icon-"] {
15
+    /* use !important to prevent issues with browser extensions that change fonts */
16
+    font-family: 'icomoon' !important;
17
+    speak: never;
18
+    font-style: normal;
19
+    font-weight: normal;
20
+    font-variant: normal;
21
+    text-transform: none;
22
+    line-height: 1;
23
+
24
+    /* Better Font Rendering =========== */
25
+    -webkit-font-smoothing: antialiased;
26
+    -moz-osx-font-smoothing: grayscale;
27
+
28
+    &.icon-cross:before {
29
+        content: "\e911";
30
+    }
31
+
32
+    &.icon-dots-three-horizontal:before {
33
+        content: "\e917";
34
+    }
35
+
36
+    &.icon-dots-three-vertical:before {
37
+        content: "\e91a";
38
+    }
39
+
40
+    &.icon-address:before {
41
+        content: "\e900";
42
+    }
43
+
44
+    &.icon-area-graph:before {
45
+        content: "\e901";
46
+    }
47
+
48
+    &.icon-calendar:before {
49
+        content: "\e902";
50
+    }
51
+
52
+    &.icon-certified:before {
53
+        content: "\e903";
54
+    }
55
+
56
+    &.icon-chat:before {
57
+        content: "\e904";
58
+    }
59
+
60
+    &.icon-checkmark:before {
61
+        content: "\e905";
62
+    }
63
+
64
+    &.icon-clock:before {
65
+        content: "\e906";
66
+    }
67
+
68
+    &.icon-cog:before {
69
+        content: "\e907";
70
+    }
71
+
72
+    &.icon-compass:before {
73
+        content: "\e908";
74
+    }
75
+
76
+    &.icon-emoji-happy:before {
77
+        content: "\e909";
78
+    }
79
+
80
+    &.icon-envelope:before {
81
+        content: "\e90a";
82
+    }
83
+
84
+    &.icon-graduation-cap:before {
85
+        content: "\e90b";
86
+    }
87
+
88
+    &.icon-guage:before {
89
+        content: "\e90c";
90
+    }
91
+
92
+    &.icon-heart:before {
93
+        content: "\e90d";
94
+    }
95
+
96
+    &.icon-home:before {
97
+        content: "\e90e";
98
+    }
99
+
100
+    &.icon-location:before {
101
+        content: "\e90f";
102
+    }
103
+
104
+    &.icon-lock:before {
105
+        content: "\e910";
106
+    }
107
+
108
+    &.icon-paper-plane:before {
109
+        content: "\e912";
110
+    }
111
+
112
+    &.icon-people:before {
113
+        content: "\e913";
114
+    }
115
+
116
+    &.icon-plus:before {
117
+        content: "\e914";
118
+    }
119
+
120
+    &.icon-price-ribbon:before {
121
+        content: "\e915";
122
+    }
123
+
124
+    &.icon-star:before {
125
+        content: "\e916";
126
+    }
127
+
128
+    &.icon-tools:before {
129
+        content: "\e918";
130
+    }
131
+
132
+    &.icon-truck:before {
133
+        content: "\e919";
134
+    }
135
+
136
+    &.icon-search:before {
137
+        content: "\e986";
138
+    }
139
+
140
+    &.icon-eye:before {
141
+        content: "\e9ce";
142
+    }
143
+
144
+    &.icon-eye-blocked:before {
145
+        content: "\e9d1";
146
+        }
147
+        
148
+
149
+    }

+ 13
- 0
frontend/assets/sass/main.scss Ver arquivo

@@ -0,0 +1,13 @@
1
+@import 'variables';
2
+@import 'icons';
3
+
4
+
5
+// Global Styles
6
+html {
7
+    background-color: $black;
8
+    font-size: $base-font-size;
9
+}
10
+body{
11
+    margin:0;
12
+    font-family: 'Source Code Pro', monospace;
13
+}

+ 48
- 0
frontend/assets/sass/variables.scss Ver arquivo

@@ -0,0 +1,48 @@
1
+// Color Variables
2
+$yellow: #F7F5A6;
3
+$light-green: #C2F279;
4
+$dark-green: #4D9127;
5
+$red: #FF3660;
6
+$light-blue: #05DBF2;
7
+$dark-blue: #183770;
8
+$dark-grey: #1F2024;
9
+$grey: #4C5264;
10
+$light-grey: #D5D5D5;
11
+$black: #000;
12
+
13
+// Define the base font size
14
+$base-font-size: 14px;
15
+
16
+// Define breakpoints
17
+$mobile: 350px;
18
+$tablet: 768px;
19
+$desktop: 960px;
20
+
21
+// Define font sizes for each breakpoint
22
+$mobile-font-size: $base-font-size;
23
+$tablet-font-size: $base-font-size * 1.2;
24
+$desktop-font-size: $base-font-size * 1.4;
25
+
26
+// Set default font size
27
+html {
28
+    font-size: $base-font-size;
29
+}
30
+
31
+// Media queries for font sizes
32
+@media (min-width: $mobile) {
33
+    html {
34
+        font-size: $mobile-font-size;
35
+    }
36
+}
37
+
38
+@media (min-width: $tablet) {
39
+    html {
40
+        font-size: $tablet-font-size;
41
+    }
42
+}
43
+
44
+@media (min-width: $desktop) {
45
+    html {
46
+        font-size: $desktop-font-size;
47
+    }
48
+}

+ 17
- 14
frontend/index.html Ver arquivo

@@ -1,16 +1,19 @@
1 1
 <!DOCTYPE html>
2 2
 <html lang="en">
3
-    <head>
4
-        <meta charset="UTF-8" />
5
-        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
-        <title>Vite App</title>
7
-    </head>
8
-    <body>
9
-        <div id="app"></div>
10
-        <script>
11
-            // DUMB shim
12
-            var global = window
13
-        </script>
14
-        <script type="module" src="/src/main.js"></script>
15
-    </body>
16
-</html>
3
+
4
+<head>
5
+    <meta charset="UTF-8" />
6
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+    <title>Siimee</title>
8
+</head>
9
+
10
+<body>
11
+    <div id="app"></div>
12
+    <script>
13
+        // DUMB shim
14
+        var global = window
15
+    </script>
16
+    <script type="module" src="/src/main.js"></script>
17
+</body>
18
+
19
+</html>

+ 620
- 4759
frontend/package-lock.json
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 4
- 0
frontend/src/App.vue Ver arquivo

@@ -73,7 +73,11 @@ export default {
73 73
 </script>
74 74
 
75 75
 <style lang="sass">
76
+@import '../assets/sass/main'
76 77
 #app > .w-app > main
77 78
     position: relative
78 79
     top: 50px
80
+    max-width: 960px
81
+    width:100%
82
+    margin: 0 auto
79 83
 </style>

+ 35
- 16
frontend/src/components/MainNav.vue Ver arquivo

@@ -1,24 +1,23 @@
1 1
 <template lang="pug">
2 2
 w-toolbar.mt6.py1(bottom fixed)
3
-    router-link.w-flex.column(:to='`/`')
4
-        w-button.pa8(bg-color='primary')
5
-            w-icon.mr1(xl) mdi mdi-home
6
-            //- p.text-upper home queue
7
-    router-link.w-flex.column(:to='`/pairs`')
8
-        w-button.pa8(bg-color='primary')
9
-            w-icon.mr1(xl) mdi mdi-tooltip-account
3
+    router-link.w-flex.column.exact-active-link(:to='`/`')
4
+        w-button.pa8()
5
+            w-icon.mr1.icon-home(xl :class="{ 'icon-selected': $route.path === '/' }")
6
+    router-link.w-flex.column.exact-active-link(:to='`/pairs`')
7
+        w-button.pa8()
8
+            w-icon.mr1.icon-people(xl, :class="{ 'icon-selected': $route.path === '/pairs' }")
10 9
             //- p.text-upper pending matches
11
-    router-link.w-flex.column(:to='`/messages`')
12
-        w-button.pa8(bg-color='primary')
13
-            w-icon.mr1(xl) mdi mdi-forum
10
+    router-link.w-flex.column.exact-active-link(:to='`/messages`')
11
+        w-button.pa8()
12
+            w-icon.mr1.icon-chat(xl, :class="{ 'icon-selected': $route.path === '/messages' }")
14 13
             //- p.text-upper active chats
15
-    router-link.w-flex.column(:to='`/onboarding`')
16
-        w-button.pa8(bg-color='primary')
17
-            w-icon.mr1(xl) mdi mdi-account-check
14
+    router-link.w-flex.column.exact-active-link(:to='`/onboarding`')
15
+        w-button.pa8()
16
+            w-icon.mr1.icon-area-graph(xl)
18 17
             //- p.text-upper survey
19
-    router-link.w-flex.column(:to='`/settings`')
20
-        w-button.pa8(bg-color='primary' disabled)
21
-            w-icon.mr1(xl) mdi mdi-cog
18
+    router-link.w-flex.column.exact-active-link(:to='`/settings`')
19
+        w-button.pa8(disabled)
20
+            w-icon.mr1.icon-cog(xl :class="{ 'icon-selected': $route.path === '/settings' }")
22 21
             //- p.text-upper settings
23 22
 </template>
24 23
 
@@ -29,3 +28,23 @@ export default {
29 28
     name: 'MainNav',
30 29
 }
31 30
 </script>
31
+
32
+<style lang="sass">
33
+@import '../assets/sass/main.scss'
34
+.w-toolbar
35
+    max-width: 960px
36
+    width:100%
37
+    margin: auto
38
+    background: $dark-grey
39
+    color: $light-grey
40
+
41
+    .w-button
42
+        background-color: $dark-grey
43
+    
44
+    .icon-selected
45
+        color: $light-green
46
+    
47
+    a
48
+        color:$light-grey
49
+</style>
50
+

+ 0
- 3
frontend/src/components/NamePlate.vue Ver arquivo

@@ -3,7 +3,6 @@
3 3
     section(:class='{ box: !isList }' v-if='pid')
4 4
         router-link(:to='`/profile/${pid}`' disabled)
5 5
             h1.text-capitalize {{ name }}
6
-                span O
7 6
             p.text-capitalize {{ role }}&nbsp;
8 7
                 span.text-capitalize(v-if='isList')
9 8
                     span.text-capitalize | {{ locale }}
@@ -48,11 +47,9 @@ export default {
48 47
         flex-direction: column
49 48
         align-items: center
50 49
         justify-content: center
51
-        padding: 15px
52 50
         min-height: 10vh
53 51
         width: 100%
54 52
         &.box
55
-            background-color: #D5D5D5
56 53
             border-radius: 6px
57 54
             height: 15vw
58 55
             width: 15vw

+ 32
- 14
frontend/src/components/PairsList.vue Ver arquivo

@@ -2,24 +2,41 @@
2 2
 section.pairs-list
3 3
     article(v-if='pairs.length')
4 4
         template(v-for='pair in pairs')
5
-            w-flex().align-center.flex-start
5
+            w-flex.align-center.flex-start
6 6
                 router-link.pair.w-flex.align-center.flex-start(
7
-    :to='`/profile/${pair.profile.pid}`')
7
+                    :to='`/profile/${pair.profile.pid}`'
8
+                )
8 9
                     .dot--icon
9 10
                     .avatar
10 11
                     .idCard
11 12
                         h3 {{ pair.profile.name }} {{ pair.profile.pid }}
12 13
                         p registered nurse
13 14
 
14
-                w-menu( left v-model='showMenu')
15
+                w-menu(left v-model='showMenu')
15 16
                     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")
17
+                        w-button.mr3(
18
+                            @click='showMenu = !showMenu'
19
+                            icon='icon-dots-three-horizontal'
20
+                        )
21
+                    w-flex
22
+                        router-link(:to='`/chat/${pair.profile.pid}`')
23
+                            w-button.mx2(
24
+                                @click='showMenu = false'
25
+                                bg-color='success'
26
+                                icon='icon-chat'
27
+                                tile
28
+                            ) Chat
29
+                        w-button.mx2.icon-calendar(
30
+                            @click='showMenu = false'
31
+                            bg-color='info'
32
+                            icon='icon-calendar'
33
+                            tile
34
+                        ) Calendar
35
+                        w-button.mx2(
36
+                            @click='showMenu = false'
37
+                            bg-color='primary'
38
+                            icon='icon-cross'
39
+                        )
23 40
 
24 41
     p(v-else) No {{ tabName }} profiles.
25 42
 </template>
@@ -40,22 +57,23 @@ const showMenu = ref(false)
40 57
 </script>
41 58
 
42 59
 <style lang="sass">
60
+@import '../assets/sass/main'
43 61
 .pairs-list
44 62
     color: #fff
45 63
     article
46
-        font-family: 'Century Gothic'
64
+        font-family: 'Source Pro'
47 65
         .dot--icon
48 66
             width:12px
49 67
             height:12px
50 68
             margin: 11px
51 69
             border-radius:50%
52
-            background-color:#60C3FF
70
+            background-color:$light-blue
53 71
         .avatar
54 72
             width:40px
55 73
             height:40px
56 74
             margin: 11px
57 75
             border-radius: 6px
58
-            background-color:#D5D5D5
76
+            background-color:$light-blue
59 77
         .idCard
60 78
             color: #fff
61 79
             margin: 11px
@@ -64,5 +82,5 @@ const showMenu = ref(false)
64 82
             p
65 83
                 font-size: 14px
66 84
 .w-menu--card
67
-    background-color: #000000 !important
85
+    background-color: $black !important
68 86
 </style>

+ 20
- 6
frontend/src/components/ProfileCard.vue Ver arquivo

@@ -14,15 +14,26 @@ w-card.profile-card-list--card.xs12
14 14
 
15 15
         template(v-if='!isList')
16 16
             w-button.text-upper.xs12.pa6(v-if='currentTab == 0 && isPaired')
17
-                w-icon.mr1(xl) mdi mdi-chat
17
+                w-icon.mr1.icon-chat(xl)
18 18
                 | start chat
19 19
 
20
+            //- TODO: Uncomment me
21
+            //- SummaryBar(
22
+            //-     :aspects='aspects'
23
+            //-     :is-tab='isPaired'
24
+            //-     :tab-content='card.summary'
25
+            //-     @tab-change='onTab'
26
+            //- )
27
+
28
+            //- This version forces tabs on
20 29
             SummaryBar(
21 30
                 :aspects='aspects'
22
-                :is-tab='isPaired'
31
+                :is-tab='true'
32
+                :name='card.name'
23 33
                 :tab-content='card.summary'
24 34
                 @tab-change='onTab'
25 35
             )
36
+
26 37
             TagList(v-if='!isPaired || isList')
27 38
 
28 39
     article.xs12.w-flex.column.justify-space-between
@@ -39,7 +50,7 @@ w-card.profile-card-list--card.xs12
39 50
             p {{ card.summary.about.tab }}
40 51
         PairingButton(@pair='onPair' @pass='onPass' v-if='!isPaired')
41 52
         w-button.text-upper.xs12.pa6(v-else-if='currentTab != 0')
42
-            w-icon.mr1(xl) mdi mdi-chat
53
+            w-icon.mr1.icon-chat(xl)
43 54
             | start chat
44 55
 </template>
45 56
 
@@ -55,6 +66,7 @@ import TagList from './TagList.vue'
55 66
 import PairingButton from './PairingButton.vue'
56 67
 
57 68
 const router = useRouter()
69
+// NOTE: toggle to use for testing pairing
58 70
 // const isPaired = ref(true)
59 71
 const isPaired = ref(false)
60 72
 
@@ -123,19 +135,21 @@ const onPass = async () => {
123 135
 </script>
124 136
 
125 137
 <style lang="sass">
138
+@import '../assets/sass/main'
139
+
126 140
 .profile-card-list--card
127
-    background-color: #000
141
+    background-color: $black
128 142
     color: #fff
129 143
     width: 100%
130 144
     max-width: 450px
131 145
     margin: 11px auto
132 146
     header > .w-button
133
-        background-color: #116006
147
+        background-color: $dark-green
134 148
         color: #fff
135 149
     footer
136 150
         margin-bottom: 22px
137 151
         p
138
-            font-family: Century Gothic, CenturyGothic, AppleGothic, sans-serif
152
+            font-family: Source Pro, AppleGothic, sans-serif
139 153
             margin: 11px auto
140 154
             padding: 0 7px
141 155
             line-height: 1.619em

+ 30
- 2
frontend/src/components/ProfileCardList.vue Ver arquivo

@@ -1,6 +1,6 @@
1 1
 <template lang="pug">
2
-section.profile-card-list.xs12.w-flex.column
3
-    article
2
+section.profile-card-list.xs12.w-flex
3
+    article.w-flex.xs-col.sm-row.sm-wrap
4 4
         ProfileCard.match-layout(
5 5
             :aspects='aspects'
6 6
             :card='card'
@@ -8,6 +8,10 @@ section.profile-card-list.xs12.w-flex.column
8 8
             :key='`${card.pid}-${i}`'
9 9
             v-for='(card, i) in cards'
10 10
         )
11
+        w-button.pa8.more-results(
12
+            @click='loadMoreResults()'
13
+            bg-color='primary'
14
+        )
11 15
 </template>
12 16
 
13 17
 <script setup>
@@ -17,6 +21,8 @@ import ProfileCard from './ProfileCard.vue'
17 21
 
18 22
 const aspects = ref(cardAspects)
19 23
 
24
+const emit = defineEmits(['loadMore'])
25
+
20 26
 const props = defineProps({
21 27
     cards: {
22 28
         type: [Object, Array],
@@ -32,10 +38,32 @@ const props = defineProps({
32 38
         ],
33 39
     },
34 40
 })
41
+
42
+const loadMoreResults = () => {
43
+    emit('loadMore')
44
+} // TODO update to scroll
35 45
 </script>
36 46
 
37 47
 <style lang="sass">
48
+@import '../assets/sass/main'
49
+
38 50
 .profile-card-list
39 51
     > header > .w-select >.primary
40 52
         margin-top: 0
53
+
54
+@media (min-width: $tablet)
55
+    section.profile-card-list.xs12.w-flex > article
56
+        display: flex
57
+        flex-wrap: wrap
58
+        flex-direction: row
59
+
60
+@media (max-width: $tablet)
61
+    section.profile-card-list.xs12.w-flex > article
62
+        display: flex
63
+        flex-wrap: wrap
64
+        flex-direction: column
65
+
66
+
67
+.more-results
68
+    margin-bottom: 2em
41 69
 </style>

+ 1
- 1
frontend/src/components/SideBar.vue Ver arquivo

@@ -5,7 +5,7 @@ aside.sidebar.w-flex.column.pa8
5 5
     nav.temp-control-box
6 6
         input(v-model="switchToPID" @keyup.enter="$emit('updatePid', switchToPID)")
7 7
         button(@click="$emit('updatePid', switchToPID)") switch profile
8
-    
8
+
9 9
 </template>
10 10
 
11 11
 <script>

+ 127
- 112
frontend/src/components/SummaryBar.vue Ver arquivo

@@ -1,112 +1,127 @@
1
-<template lang="pug">
2
-section.w-flex.column.pb5
3
-    nav.fill-width.w-flex.column.justify-space-between
4
-        // Tabbed Layout
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 }')
13
-                .w-flex.column.justify-start
14
-                    p(v-if='tabContent[item].matchPerc') {{ tabContent[item].matchPerc }}%
15
-                    p(v-else) &nbsp;
16
-                    p {{ item }}
17
-            // About Tab
18
-            template(#item-content.1='{ item }')
19
-                .tab--about
20
-                    p {{ tabContent[item].tab }}
21
-                    br
22
-                    p {{ tabContent[item].tab }}
23
-                    br
24
-                    hr
25
-
26
-            // Passion Tab
27
-            template(#item-content.2='{ item }')
28
-                .tab--passion
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
-                    )
37
-
38
-            // Aspirations Tab
39
-            template(#item-content.3='{ item }')
40
-                .tab--aspirations
41
-                    p {{ tabContent[item].tab }}
42
-
43
-            // Skills Tab
44
-            template(#item-content.4='{ item }')
45
-                .tab--skills
46
-                    p {{ tabContent[item].tab }}
47
-
48
-        // Untabbed Layout
49
-        ul.w-flex.row.justify-space-between(v-else)
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
56
-                    .w-flex.column.justify-start
57
-                        p 
58
-                            span {{ tabContent[item].matchPerc }}%
59
-                        p.text-capitalize {{ item }}
60
-</template>
61
-
62
-<script>
63
-import SpiderChart from './SpiderChart.vue'
64
-
65
-export default {
66
-    components: { SpiderChart },
67
-    props: {
68
-        aspects: {
69
-            required: true,
70
-            type: Array,
71
-        },
72
-        tabContent: {
73
-            required: true,
74
-            type: Object,
75
-        },
76
-        isTab: {
77
-            required: false,
78
-            type: Boolean,
79
-            default: false,
80
-        },
81
-        showIcon: {
82
-            required: false,
83
-            type: Boolean,
84
-            default: true,
85
-        },
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
-    },
97
-}
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>
1
+<template lang="pug">
2
+section.w-flex.column.pb5
3
+    nav.fill-width.w-flex.column.justify-space-between
4
+        // Tabbed Layout
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 }')
13
+                .w-flex.column.justify-start
14
+                    p(v-if='tabContent[item].matchPerc') {{ tabContent[item].matchPerc }}%
15
+                    p(v-else) &nbsp;
16
+                    p {{ item }}
17
+
18
+            // About Tab
19
+            template(#item-content.1='{ item }')
20
+                .tab--about
21
+                    p {{ tabContent[item].tab }}
22
+                    br
23
+                    p {{ tabContent[item].tab }}
24
+                    br
25
+                    hr
26
+
27
+            // Passion Tab
28
+            template(#item-content.2='{ item }')
29
+                .tab--passion
30
+                    p {{ tabContent[item].tab }}
31
+                    SpiderChart(
32
+                        :labels='aspects.map(label => label.name)'
33
+                        :profile-data='aspects.map(data => data.percentage * 10)'
34
+                        :profile-name='name'
35
+                        :target-data='targetScore'
36
+                        v-if='isTab'
37
+                    )
38
+
39
+            // Aspirations Tab
40
+            template(#item-content.3='{ item }')
41
+                .tab--aspirations
42
+                    p {{ tabContent[item].tab }}
43
+
44
+            // Skills Tab
45
+            template(#item-content.4='{ item }')
46
+                .tab--skills
47
+                    p {{ tabContent[item].tab }}
48
+
49
+        // Untabbed Layout
50
+        ul.w-flex.row.justify-space-between(v-else)
51
+            template(
52
+                :key='index'
53
+                v-for='(item, index) in Object.keys(tabContent)'
54
+            )
55
+                li.w-flex.row(v-if='item !== "about"')
56
+                    w-icon.mr1.icon-compass(xl)
57
+                    .w-flex.column.justify-start
58
+                        p
59
+                            span {{ tabContent[item].matchPerc }}%
60
+                        p.text-capitalize {{ item }}
61
+</template>
62
+
63
+<script>
64
+import SpiderChart from './SpiderChart.vue'
65
+import { currentProfile } from '../services'
66
+
67
+export default {
68
+    components: { SpiderChart },
69
+    props: {
70
+        aspects: {
71
+            required: true,
72
+            type: Array,
73
+        },
74
+        tabContent: {
75
+            required: true,
76
+            type: Object,
77
+        },
78
+        name: {
79
+            required: true,
80
+            type: String,
81
+        },
82
+        isTab: {
83
+            required: false,
84
+            type: Boolean,
85
+            default: false,
86
+        },
87
+        showIcon: {
88
+            required: false,
89
+            type: Boolean,
90
+            default: true,
91
+        },
92
+    },
93
+    emits: ['tab-change'],
94
+    computed: {
95
+        targetScore() {
96
+            try {
97
+                let aspectResponses = currentProfile._profile.responses.filter(
98
+                    r => [1, 2, 3, 4, 5, 6].indexOf(r.response_key_id) !== -1,
99
+                )
100
+                return aspectResponses.map(r => Number(r.val))
101
+            } catch (e) {
102
+                console.warn('error: No aspect responses for current profile.')
103
+                return [1, 1, 1, 1, 1, 1]
104
+            }
105
+        },
106
+    },
107
+    methods: {
108
+        onTabChanged(tabs) {
109
+            this.$emit('tab-change', tabs)
110
+        },
111
+    },
112
+}
113
+</script>
114
+<style lang="sass">
115
+section
116
+    font-family: Century Gothic, CenturyGothic, AppleGothic, sans-serif
117
+    ul
118
+        margin: 11px 0
119
+        li
120
+            margin: 0 7px
121
+            font-size: .8em
122
+            p > span
123
+                font-weight: bold
124
+                font-size: 1em
125
+        li:not(:last-child)
126
+            border-right: 1px solid #fff
127
+</style>

+ 3
- 3
frontend/src/components/TopNav.vue Ver arquivo

@@ -2,17 +2,17 @@
2 2
 w-toolbar.top-nav.w-flex.align-center.justify-between(top fixed)
3 3
     router-link.w-flex.column.no-grow(v-if="$route.params.pid" :to='`/`')
4 4
         w-button.pa4(bg-color='transparent')
5
-            w-icon.mr1(md) mdi mdi-arrow-left
5
+            w-icon.mr1.icon-arrow-left2(md)
6 6
     
7 7
     w-button.pa4(v-if="!$route.params.pid" bg-color='transparent' @click="$emit('on-open')")
8
-        w-icon.mr1(md) mdi mdi-cog
8
+        w-icon.mr1.icon-cog(md)
9 9
 
10 10
     router-link.w-flex.column(:to='`/`')
11 11
         p.text-upper.text-center {{$route.path}} {{$route.params}}
12 12
 
13 13
     router-link.w-flex.column.no-grow(v-if="!$route.params.pid" :to='`/search`')
14 14
         w-button.pa4(bg-color='transparent' disabled)
15
-            w-icon.mr1(md) mdi mdi-magnify
15
+            w-icon.mr1.icon-search(md)
16 16
                 //- p.text-upper settings
17 17
 </template>
18 18
 

+ 2
- 2
frontend/src/components/onboarding/AccountType.vue Ver arquivo

@@ -2,10 +2,10 @@
2 2
 w-card.w-flex.column
3 3
     p {Name}, let's get started on your profile while we verify your account
4 4
     w-button.search-type(@click='handleSubmit("Recruiter")')
5
-        w-icon.mr1 mdi mdi-account-multiple
5
+        w-icon.mr1.icon-people
6 6
         | CANDIDATES
7 7
     w-button.search-type(@click='handleSubmit("Jobseeker")')
8
-        w-icon.mr1 mdi mdi-bookmark-box-multiple
8
+        w-icon.mr1.icon-bokmarks
9 9
         | JOBS
10 10
 </template>
11 11
 

+ 0
- 48
frontend/src/components/onboarding/Aspects.vue Ver arquivo

@@ -1,48 +0,0 @@
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>

+ 107
- 0
frontend/src/components/onboarding/Auth.vue Ver arquivo

@@ -0,0 +1,107 @@
1
+<template lang="pug">
2
+.wait-message
3
+    p.verify-message Thanks for signing up!
4
+    p.verify-message We'll just need you to verify your email address to continue. Please check your email!
5
+</template>
6
+
7
+<script>
8
+import { Authenticator } from '../../services/auth.service.js'
9
+import { createProfileForUserId } from '../../services/profile.service'
10
+import { signupUser } from '../../services/user.service.js'
11
+
12
+export default {
13
+    name: 'Auth',
14
+    props: {
15
+        question: {
16
+            required: true,
17
+            type: Object,
18
+            default: () => {},
19
+        },
20
+        answered: {
21
+            type: Object,
22
+            default: () => {},
23
+        },
24
+        responses: {
25
+            type: Object,
26
+            default: () => {},
27
+        },
28
+        survey: {
29
+            required: true,
30
+            type: Object,
31
+            default: () => {},
32
+        },
33
+    },
34
+    emits: ['update-answers'],
35
+    data: () => ({
36
+        authenticator: {},
37
+    }),
38
+    async created() {
39
+        // Establishes New User And Sends Auth Email
40
+        this.authenticator = new Authenticator()
41
+        try {
42
+            this.doesUserHaveMinResponses(this.responses)
43
+            const userPass = this.responses.find(
44
+                response => response.response_key_id === 9,
45
+            )
46
+            const newUserId = await this.signupNewUser({
47
+                ...this.answered,
48
+                password: userPass.val,
49
+            })
50
+            await this.createProfileForNewUser(newUserId, this.responses)
51
+            const accessToken = await this.getAccessToken({
52
+                ...this.answered,
53
+            })
54
+            console.log('accessToken :=>', accessToken)
55
+            const sessionInfo = await this.authenticator.sendAuthEmail({
56
+                ...this.answered,
57
+                accessToken: accessToken,
58
+            })
59
+            document.cookie = `siimee_access=${sessionInfo.hashedAccessToken}; max-age=600; path=/; secure`
60
+        } catch (err) {
61
+            // TODO: render an error page in this component displaying which
62
+            // error occurred and how to reach out to staff
63
+            console.error('ERROR :=>', err)
64
+        }
65
+    },
66
+    methods: {
67
+        doesUserHaveMinResponses(responses) {
68
+            if (!this.survey.hasMinResponsesToCreateProfile(responses))
69
+                throw new Error(
70
+                    'User has not answered minimum amount of questions to create profile',
71
+                )
72
+        },
73
+        async getAccessToken(payload) {
74
+            return await this.authenticator.getAccessToken({
75
+                payload,
76
+            })
77
+        },
78
+        async signupNewUser(userInfo) {
79
+            const newUser = await signupUser(userInfo)
80
+            if (!newUser || newUser.error) {
81
+                throw new Error(
82
+                    'Error occured when signing up new User :=>',
83
+                    newUser.error,
84
+                )
85
+            } else return newUser.user_id
86
+        },
87
+        async createProfileForNewUser(userId, responses) {
88
+            try {
89
+                await createProfileForUserId(userId, responses)
90
+            } catch (err) {
91
+                throw new Error(err)
92
+            }
93
+        },
94
+    },
95
+}
96
+</script>
97
+
98
+<style>
99
+.wait-message {
100
+    margin: 5rem auto;
101
+    text-align: center;
102
+    width: 90%;
103
+    max-width: 35rem;
104
+    font-size: 150%;
105
+    font-weight: bold;
106
+}
107
+</style>

+ 13
- 7
frontend/src/components/onboarding/FormDropdown.vue Ver arquivo

@@ -1,8 +1,10 @@
1 1
 <template lang="pug">
2 2
 .role
3
-    h3 {{ question.response_key_category }}
4
-    p {{ question.response_key_prompt }}
3
+    span(style='text-align: center') {{ parsedPrompt.start }}
4
+    span(style='text-align: center') {{ parsedPrompt.mid }}
5 5
     w-select.mt4(:items='items' placeholder='i am' v-model='selection')
6
+    br
7
+    p(style='text-align: center') {{ parsedPrompt.end }}
6 8
     w-button.ma1.grow(@click='handleSubmit') NEXT
7 9
 </template>
8 10
 
@@ -18,21 +20,25 @@ export default {
18 20
     emits: ['update-answers'],
19 21
     data: () => ({
20 22
         selection: null,
23
+        parsedPrompt: {},
21 24
     }),
22 25
     computed: {
23 26
         items() {
24 27
             return this.question.responses.map(res => ({ label: res }))
25 28
         },
26 29
     },
30
+    created() {
31
+        const parsedPromptArr =
32
+            this.question.response_key_prompt.split('[break]')
33
+        this.parsedPrompt.start = parsedPromptArr[0]
34
+        this.parsedPrompt.mid = parsedPromptArr[1]
35
+        this.parsedPrompt.end = parsedPromptArr[2]
36
+    },
27 37
     methods: {
28 38
         handleSubmit() {
29
-            if (!this.selection) {
30
-                console.warn('Please select a role.')
31
-                return
32
-            }
33 39
             let payload = {
34 40
                 question: this.question,
35
-                answer: this.selection,
41
+                input: this.selection,
36 42
             }
37 43
             this.$emit('update-answers', payload)
38 44
         },

+ 62
- 11
frontend/src/components/onboarding/FormInput.vue Ver arquivo

@@ -1,9 +1,40 @@
1 1
 <template lang="pug">
2 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
3
+    span(style='text-align: center') {{ parsedPrompt.start }}
4
+    br
5
+    br
6
+    span(style='text-align: center') {{ parsedPrompt.mid }}
7
+    input(
8
+        :placeholder='question.placeholder'
9
+        @keyup.enter='handleSubmit({ question, input })'
10
+        type='text'
11
+        v-focus
12
+        v-if='question.survey_stage !== "image" && question.survey_stage !== "blurb" && question.survey_stage !== "password"'
13
+        v-model='input'
14
+    )
15
+    input.pass(
16
+        :placeholder='question.placeholder'
17
+        @keyup.enter='handleSubmit({ question, input })'
18
+        style='-webkit-text-security: circle'
19
+        type='password'
20
+        v-else-if='question.survey_stage === "password"'
21
+        v-focus
22
+        v-model='input'
23
+    )
24
+    w-button.ma1.grow(
25
+        @click='submitImage'
26
+        v-else-if='question.survey_stage === "image"'
27
+    ) UPLOAD IMAGE
28
+    textarea(
29
+        :placeholder='`${question.placeholder}`'
30
+        @keyup.enter='handleSubmit({ question, input })'
31
+        cols='50'
32
+        rows='4'
33
+        v-else-if='question.survey_stage === "blurb"'
34
+        v-model='input'
35
+    )
36
+    span(style='text-align: center') {{ parsedPrompt.end }}
37
+    w-button.ma1.grow(@click='handleSubmit({ question, input })') NEXT
7 38
 </template>
8 39
 <script>
9 40
 export default {
@@ -17,20 +48,40 @@ export default {
17 48
     emits: ['update-answers'],
18 49
     data: () => ({
19 50
         input: null,
51
+        parsedPrompt: {},
20 52
     }),
53
+    created() {
54
+        const parsedPromptArr =
55
+            this.question.response_key_prompt.split('[break]')
56
+        this.parsedPrompt.start = parsedPromptArr[0]
57
+        this.parsedPrompt.mid = parsedPromptArr[1]
58
+        this.parsedPrompt.end = parsedPromptArr[2]
59
+    },
21 60
     methods: {
22
-        handleSubmit() {
23
-            if(this.question.response_key_prompt === 'password') {
24
-                this.$emit('update-answers') // no password collection
25
-                return
26
-            }
27
-
61
+        handleSubmit(answerInfo) {
62
+            this.$emit('update-answers', answerInfo)
63
+        },
64
+        submitImage() {
28 65
             let payload = {
29 66
                 question: this.question,
30
-                answer: this.input,
67
+                input: 'placeholder for image',
31 68
             }
32 69
             this.$emit('update-answers', payload)
33 70
         },
34 71
     },
35 72
 }
36 73
 </script>
74
+
75
+<style>
76
+.form-input,
77
+input[placeholder],
78
+textarea[placeholder] {
79
+    text-align: center;
80
+}
81
+input {
82
+    border: 0;
83
+    outline: 0;
84
+    background: transparent;
85
+    border-bottom: 1px solid black;
86
+}
87
+</style>

+ 37
- 13
frontend/src/components/onboarding/QuestionResponse.vue Ver arquivo

@@ -1,9 +1,20 @@
1 1
 <template lang="pug">
2 2
 w-card.question
3
-    p {{question.question}} 
3
+    p {{ question.response_key_prompt }}
4 4
     section.radio-buttons.w-flex.row.justify-space-between
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")
5
+        p(v-for='label in question.labels') {{ label }}
6
+    w-radios.w-flex.row.justify-space-between(
7
+        :items='radioItems'
8
+        @update:model-value='onUpdate'
9
+        color='red'
10
+    )
11
+    w-button.ma1.grow(
12
+        @click='handleSubmit'
13
+        @keyup.enter='handleSubmit'
14
+        v-if='currentStep !== surveyStepsCount'
15
+    ) NEXT
16
+    w-button.ma1.grow(@click='handleSubmit' v-else) SUBMIT ANSWERS
17
+    p(v-if='noChoiceMade') Tough choices, we know! Just answer to the best of your ability. The team over at Siimee values the answers you provide us so that we can show you results catered to your specific needs.
7 18
 </template>
8 19
 
9 20
 <script>
@@ -13,20 +24,33 @@ export default {
13 24
             type: Object,
14 25
             required: true,
15 26
         },
27
+        currentStep: {
28
+            type: Number,
29
+            required: true,
30
+        },
31
+        surveyStepsCount: {
32
+            type: Number,
33
+            required: true,
34
+        },
16 35
     },
17
-    emits: ['updated'],
36
+    emits: ['update-answers'],
18 37
     data: () => ({
19
-        radioItems: [
20
-            { answer: 1 },
21
-            { answer: 2 },
22
-            { answer: 3 },
23
-            { answer: 4 },
24
-            { answer: 5 },
25
-        ],
38
+        radioItems: [1, 2, 3, 4, 5],
39
+        answer: null,
40
+        noChoiceMade: null,
26 41
     }),
27 42
     methods: {
28
-        onUpdate(e) {
29
-            this.$emit('updated', { ...this.question, answer: e + 1 })
43
+        onUpdate(index) {
44
+            this.noChoiceMade = false
45
+            this.answer = this.radioItems[index]
46
+        },
47
+        handleSubmit() {
48
+            const payload = {
49
+                question: this.question,
50
+                input: this.answer,
51
+            }
52
+            this.noChoiceMade = payload.input === null ? true : false
53
+            this.$emit('update-answers', payload)
30 54
         },
31 55
     },
32 56
 }

+ 12
- 2
frontend/src/components/onboarding/Splash.vue Ver arquivo

@@ -5,15 +5,25 @@ w-flex.column
5 5
         :src='`/assets/logos/siimee_logo.jpg`'
6 6
         :width='300'
7 7
     )
8
+
8 9
     w-button.ma1.grow.next-btn(
9 10
         :height='50'
10 11
         :width='315'
11
-        @click='this.$emit("go-to-step", currentStep + 1)'
12
+        @click='handleSubmit'
12 13
         bg-color='success'
13 14
         shadow
14 15
         text
15 16
         xl
16
-    ) GET STARTED
17
+    ) SIGN UP
18
+    router-link(to='/login')
19
+        w-button.ma1.grow.next-btn(
20
+            :height='50'
21
+            :width='315'
22
+            bg-color='success'
23
+            shadow
24
+            text
25
+            xl
26
+        ) LOG IN
17 27
 </template>
18 28
 
19 29
 <script>

+ 4
- 2
frontend/src/components/onboarding/index.js Ver arquivo

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

+ 26
- 1
frontend/src/entities/card/card.js Ver arquivo

@@ -1,4 +1,5 @@
1 1
 /** @module card/card */
2
+import { aspectsArr } from '../../utils/lang.js'
2 3
 
3 4
 const DEFAULT_ABOUT =
4 5
     '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.'
@@ -40,7 +41,7 @@ class SummaryGroup {
40 41
 }
41 42
 
42 43
 class Aspect {
43
-    constructor({ name, labels, percentage = 50 }) {
44
+    constructor({ name, labels, percentage = 1 }) {
44 45
         this.name = name
45 46
         this.labels = labels
46 47
         this.percentage = percentage
@@ -73,6 +74,15 @@ const cardAspects = [
73 74
     }),
74 75
 ]
75 76
 
77
+const responseKeyIdToAspectName = {
78
+    1: 'vision',
79
+    2: 'creativity',
80
+    3: 'dynamism',
81
+    4: 'precision',
82
+    5: 'focus',
83
+    6: 'attention',
84
+}
85
+
76 86
 /**
77 87
  * Class representing a profile card
78 88
  * NOT to be confused with a profile
@@ -123,6 +133,21 @@ const makeCardFromProfile = profile => {
123 133
     c.ethinicity = profile?.profile_prefs?.ethnicity?.val
124 134
     c.locale = `${profile.city}, ${profile.state}`
125 135
     c.email = profile.user_email
136
+
137
+    let aspectResponses = profile?.responses.filter(
138
+        r => aspectsArr.indexOf(r.response_key_id) !== -1,
139
+    )
140
+    if (aspectResponses.length) {
141
+        // if user has responses for aspects we overwrite default percentages
142
+        c.aspects.map(a => {
143
+            a.percentage = Number(
144
+                aspectResponses.find(
145
+                    r => responseKeyIdToAspectName[r.response_key_id] == a.name,
146
+                ).val,
147
+            )
148
+            return a
149
+        })
150
+    }
126 151
     // TODO: delete me later
127 152
     if (profile.profile_description) {
128 153
         c.summary.updateTab('about', profile.profile_description)

+ 2
- 0
frontend/src/entities/profile/profile.schema.js Ver arquivo

@@ -27,6 +27,8 @@ const profileSchema = {
27 27
         user_name: Joi.string(),
28 28
         user_id: Joi.number(),
29 29
         user_email: Joi.string(),
30
+        image: Joi.any(),
31
+        blurb: Joi.any(),
30 32
         profile_id: Joi.number(),
31 33
         profile_description: Joi.string().allow(null, ''),
32 34
         profile_media: Joi.array().items(Joi.string()),

+ 3
- 1
frontend/src/entities/response/response.schema.js Ver arquivo

@@ -8,7 +8,9 @@ const responseSchema = Joi.object({
8 8
     profile_id: Joi.number(),
9 9
     response_id: Joi.number(),
10 10
     response_key_id: Joi.number(),
11
-    val: Joi.string(),
11
+    // TODO: troubleshoot later, suppressed validation warning for now
12
+    // val: Joi.string(),
13
+    val: Joi.any(),
12 14
 })
13 15
 
14 16
 export { responseSchema }

+ 39
- 0
frontend/src/entities/survey/survey.answer.validator.js Ver arquivo

@@ -0,0 +1,39 @@
1
+import Joi from 'joi'
2
+import domains from './tlds-alpha-by-domain.js'
3
+
4
+const answerValidator = {
5
+    name: Joi.string().min(2).max(50).required(),
6
+    email: Joi.string().email({
7
+        minDomainSegments: 2,
8
+        tlds: { allow: domains },
9
+    }),
10
+    // TODO: change to valdate against JWT??
11
+    auth: Joi.any(),
12
+
13
+    // TODO: Uncomment when ease of development no longer needed
14
+    // password: Joi.string().min(10).max(30).pattern(new RegExp('[a-zA-Z0-9]+')),
15
+    password: Joi.string().max(30).pattern(new RegExp('[a-zA-Z0-9]+')),
16
+    // TODO: Change if going international (only works in usa)
17
+    zipcode: Joi.string().min(5).max(5).pattern(new RegExp('^[0-9]{5}$')),
18
+    seeking: Joi.string(),
19
+    urgency: Joi.string(),
20
+    presence: Joi.string(),
21
+    duration: Joi.string(),
22
+    pronouns: Joi.string(),
23
+    language: Joi.string(),
24
+    image: Joi.any(),
25
+    // NOTE: Allows 1 to 3 digits and then distance metric
26
+    distance: Joi.string()
27
+        .min(4)
28
+        .max(15)
29
+        .pattern(
30
+            new RegExp(
31
+                '^\\d{1,3}(\\.\\d{1,2})?\\s?(mi|km|mile|miles|kilometer|kilometers)$',
32
+            ),
33
+        ),
34
+    blurb: Joi.string().max(200),
35
+    value: Joi.string(),
36
+    aspect: Joi.number(),
37
+}
38
+
39
+export { answerValidator }

+ 28
- 2
frontend/src/entities/survey/survey.js Ver arquivo

@@ -1,8 +1,10 @@
1 1
 /** @module survey/survey */
2 2
 import { _baseRecord } from '../index.js'
3 3
 import { surveySchema } from './survey.schema.js'
4
+import { answerValidator } from './survey.answer.validator.js'
5
+import { aspectsArr } from '../../utils/lang.js'
4 6
 
5
-const SCORED = [1, 2, 3, 4, 5, 6]
7
+const SCORED = aspectsArr
6 8
 const _isScored = id => SCORED.includes(id)
7 9
 const _makeCategoryFriendly = responseCategory => {
8 10
     const labels = responseCategory.split('_vs_')
@@ -35,7 +37,31 @@ class Survey extends _baseRecord {
35 37
         /**  Fields */
36 38
         this.steps = [...questionSteps] // ! required
37 39
         this.aspectQuestions = _formatAspectQuestions(this.steps)
38
-        console.log('this.aspectQuestions: ', JSON.stringify(this.aspectQuestions))
40
+    }
41
+
42
+    hasMinResponsesToCreateProfile(responses) {
43
+        const neededResponseKeys = [8, 7, 11, 9]
44
+        const hasNeededResponseKey = responses => {
45
+            return responses.every(response => {
46
+                neededResponseKeys.includes(response.response_key_id)
47
+            })
48
+        }
49
+        return hasNeededResponseKey
50
+    }
51
+
52
+    validateAnswer(payload) {
53
+        const { question, input } = payload
54
+
55
+        // Continue our ugly hacks
56
+        const validationType =
57
+            question.category == 'aspect'
58
+                ? question.category
59
+                : question.survey_stage
60
+        const validate = answerValidator[validationType].validate(input)
61
+        if (validate.error) {
62
+            console.error(`error: ${validate.error}`)
63
+        }
64
+        return !validate.error ? true : false
39 65
     }
40 66
 
41 67
     isValid() {

+ 1486
- 0
frontend/src/entities/survey/tlds-alpha-by-domain.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 10
- 1
frontend/src/router/index.js Ver arquivo

@@ -7,6 +7,7 @@ import PairsView from '../views/PairsView.vue'
7 7
 import LoginView from '../views/LoginView.vue'
8 8
 import SurveyView from '../views/SurveyView.vue'
9 9
 import OnboardingView from '../views/OnboardingView.vue'
10
+import VerifyView from '../views/VerifyView.vue'
10 11
 import MessagesView from '../views/MessagesView.vue'
11 12
 
12 13
 const routes = [
@@ -57,12 +58,20 @@ const routes = [
57 58
         name: `SurveyView`,
58 59
         meta: { requiresAuth: true, requiresCompleteProfile: false },
59 60
     },
61
+    // TODO: remove after better implementation is found for verifying email
62
+    // hash
60 63
     {
61
-        path: `/onboarding`,
64
+        path: `/onboarding/`,
62 65
         component: OnboardingView,
63 66
         name: `OnboardingView`,
64 67
         meta: { requiresAuth: true, requiresCompleteProfile: false },
65 68
     },
69
+    {
70
+        path: `/verify/:hashedToken?`,
71
+        component: VerifyView,
72
+        name: `VerifyView`,
73
+        meta: { requiresAuth: true, requiresCompleteProfile: false },
74
+    },
66 75
     {
67 76
         path: `/login`,
68 77
         component: LoginView,

+ 15
- 1
frontend/src/services/auth.service.js Ver arquivo

@@ -1,7 +1,21 @@
1
+import { db } from '../utils/db.js'
2
+
1 3
 class Authenticator {
2 4
     constructor() {
3 5
         this.curentUser = null
4 6
     }
7
+    async sendAuthEmail(answered) {
8
+        return await db.post('/user/sendemail/', answered)
9
+    }
10
+    async verifyAuthSession(hashedToken) {
11
+        return await db.get(`/user/verify/${hashedToken}`)
12
+    }
13
+    async getAccessToken(req) {
14
+        return await db.post('/user/getaccess', req, true)
15
+    }
16
+    async validateSession(hashedAccessToken) {
17
+        return await db.post('/user/validatesession', hashedAccessToken, true)
18
+    }
5 19
 }
6 20
 
7
-export { Authenticator }
21
+export { Authenticator }

+ 4
- 1
frontend/src/services/chat.service.js Ver arquivo

@@ -116,7 +116,10 @@ class Chatter {
116 116
         } catch (error) {
117 117
             console.error('[chatter]', error)
118 118
         }
119
-        const channelHistory = pastMessages.channels[channel]
119
+        const channelHistory =
120
+            pastMessages && pastMessages.channels
121
+                ? pastMessages.channels[channel]
122
+                : null
120 123
         console.log('channelHistory :>> ', channelHistory)
121 124
         return channelHistory
122 125
             ? channelHistory.map(msg => ({

+ 3
- 1
frontend/src/services/grouping.service.js Ver arquivo

@@ -1,6 +1,6 @@
1 1
 import { db } from '../utils/db.js'
2 2
 import { Grouping, Profile } from '../entities/index.js'
3
-
3
+import { ref } from 'vue'
4 4
 /**
5 5
  * Get Memberships associated with a single Profile from the database and
6 6
  * create a class from the data and
@@ -15,6 +15,7 @@ const fetchMembershipsByProfileId = async profileId => {
15 15
         memberships = await db.get(`/membership/${profileId}`)
16 16
         for (let membership of memberships) {
17 17
             const grouping = new Grouping(membership)
18
+            // TODO: look here to see about current reveal issue -bh
18 19
             if (grouping.isValid()) {
19 20
                 // Reformat incoming profile data into Profile entity
20 21
                 grouping.profile = new Profile(grouping.profile)
@@ -25,6 +26,7 @@ const fetchMembershipsByProfileId = async profileId => {
25 26
                     `/profile/${profileId}/tags/${grouping.grouping_id}`,
26 27
                 )
27 28
                 grouping.tags = [...targetTags, ...profileTags]
29
+                grouping.revealedFromNotification = ref([])
28 30
                 grouping._loading.value = false
29 31
                 validGroupingInstances.push(grouping)
30 32
             }

+ 1
- 0
frontend/src/services/login.service.js Ver arquivo

@@ -50,6 +50,7 @@ class Login {
50 50
      * @returns {boolean}
51 51
      */
52 52
     get isComplete() {
53
+        // TODO: remove once Vue Router guards allow to redirect via url
53 54
         return (
54 55
             this.responses.length == surveyFactory.questionsFromDb.length &&
55 56
             surveyFactory.questionsFromDb.length > 0

+ 31
- 2
frontend/src/services/notification.service.js Ver arquivo

@@ -1,5 +1,5 @@
1 1
 import { remote } from '../utils/db.js'
2
-
2
+import { currentProfile } from '../services'
3 3
 /**
4 4
  * Base notifier class
5 5
  * @param {number} profileId needed to listen for events for this profile
@@ -29,12 +29,41 @@ class StonkAlert extends Toaster {
29 29
         this.stonks = {}
30 30
         this.listenFor(`${profileId}.${this.event}`, message => {
31 31
             const parsed = JSON.parse(message.data)
32
+            if (parsed.name === 'REVEALED_INFO') {
33
+                this._appendTagsToGrouping(parsed)
34
+            }
32 35
             this.stonks[parsed.name] = parsed
33 36
             waveCb(this._formatToast(parsed), parsed.type)
34 37
         })
35 38
     }
36 39
     _formatToast(parsed) {
37
-        return `${parsed.name}: ${parsed.profile_id} ${parsed.order} at ${parsed.price}`
40
+        if (parsed.revealed_info) {
41
+            return `${parsed.name}: ${parsed.revealed_info} at ${parsed.type}: ${parsed.url}`
42
+        } else if (parsed.url) {
43
+            return `${parsed.name}: ${parsed.profile_id}: visit: ${parsed.url}`
44
+        } else {
45
+            return `${parsed.name}: ${parsed.profile_id} ${parsed.order} at ${parsed.price}`
46
+        }
47
+    }
48
+    _appendTagsToGrouping(parsed) {
49
+        const foundGrouping = currentProfile.groupings.find(
50
+            grouping => grouping.grouping_id === parsed.grouping_id,
51
+        )
52
+        if (foundGrouping) {
53
+            const tagFromNotification = {
54
+                is_active: 1,
55
+                tag_category: 'reveal',
56
+                profile_id: parsed.profile_id,
57
+                tag_description: parsed.description,
58
+                tag_id: parsed.tag,
59
+            }
60
+            const target_desc = parsed.description
61
+            tagFromNotification[target_desc] = parsed.revealed_info
62
+            foundGrouping.profile.reveal.push(tagFromNotification)
63
+            foundGrouping.revealedFromNotification.value.push(
64
+                tagFromNotification,
65
+            )
66
+        }
38 67
     }
39 68
 }
40 69
 

+ 7
- 4
frontend/src/services/profile.service.js Ver arquivo

@@ -8,8 +8,11 @@ import { Profile } from '../entities/profile/profile.js'
8 8
  * @param {number} userId
9 9
  * @returns {array} instantiated Profile objects (see: /entites/profile)
10 10
  */
11
-const fetchProfilesByUserId = async userId => {
12
-    const profilesForUserId = await db.get(`/user/${userId}/profiles`)
11
+const fetchProfilesByUserId = async (userId, sessionToken) => {
12
+    const profilesForUserId = await db.get(
13
+        `/user/${userId}/profiles`,
14
+        sessionToken,
15
+    )
13 16
     const validProfileInstances = []
14 17
     for (let profileData of profilesForUserId) {
15 18
         const profile = new Profile(profileData)
@@ -25,10 +28,10 @@ const createProfileForUserId = async (userId, responses) => {
25 28
     return profile
26 29
 }
27 30
 
28
-const fetchProfileByProfileId = async profileId => {
31
+const fetchProfileByProfileId = async (profileId, sessionToken) => {
29 32
     let profile
30 33
     try {
31
-        const profileData = await db.get(`/profile/${profileId}`)
34
+        const profileData = await db.get(`/profile/${profileId}`, sessionToken)
32 35
         profile = new Profile(profileData)
33 36
         if (!profile.isValid()) {
34 37
             throw '[Profile Service error]: Invalid or incomplete profile returned.'

+ 6
- 2
frontend/src/services/queue.service.js Ver arquivo

@@ -4,12 +4,16 @@ import { Profile } from '../entities/index.js'
4 4
 /**
5 5
  * Get a match queue of profiles
6 6
  * @param {number} profileId
7
+ * @param {number} limit
8
+ * @param {number} offset
7 9
  * @returns {array} profiles
8 10
  */
9
-const fetchQueueByProfileId = async profileId => {
11
+const fetchQueueByProfileId = async (profileId, offset = 0, limit = 5) => {
10 12
     let queue
11 13
     try {
12
-        queue = await db.get(`/profile/${profileId}/queue?include_profile=true`)
14
+        queue = await db.get(
15
+            `/profile/${profileId}/queue?include_profile=true&limit=${limit}&offset=${offset}`,
16
+        )
13 17
         if (!queue?.length) {
14 18
             throw '[Queue Service]: Could not retrieve match queue.\nHave you taken the survey and scored this profile?'
15 19
         }

+ 20
- 2
frontend/src/services/survey.service.js Ver arquivo

@@ -22,6 +22,17 @@ const fetchQuestions = async () => {
22 22
     return withResponses
23 23
 }
24 24
 
25
+const insertNewSurveyResponse = async (surveyResponse, profileId) => {
26
+    const keyId = surveyResponse.response_key_id
27
+    const val = surveyResponse.val
28
+    // POST
29
+    db.post(`/profile/${profileId}/insert/${keyId}`, {
30
+        profile_id: profileId,
31
+        response_key_id: keyId,
32
+        val: val,
33
+    })
34
+}
35
+
25 36
 const updateSurveyByProfileId = async (surveyResponses, profileId) => {
26 37
     surveyResponses.forEach(responseKeyIdwithVal => {
27 38
         const keyId = responseKeyIdwithVal.response_key_id
@@ -38,9 +49,15 @@ const updateSurveyByProfileId = async (surveyResponses, profileId) => {
38 49
     })
39 50
 }
40 51
 
41
-const scoreSurveyByProfileId = async (profileId, maxDistance = 99) => {
52
+const scoreSurveyByProfileId = async (
53
+    profileId,
54
+    maxDistance = 99,
55
+    duration,
56
+    presence,
57
+    certifications,
58
+) => {
42 59
     const scoreSurvey = await db.get(
43
-        `/profile/${profileId}/score?max_distance=${maxDistance}`,
60
+        `/profile/${profileId}/score?max_distance=${maxDistance}&duration=${duration}&presence=${presence}&certifications=${certifications}`,
44 61
     )
45 62
     return scoreSurvey
46 63
 }
@@ -51,6 +68,7 @@ const fetchResponsesByProfileId = async profileId => {
51 68
 
52 69
 export {
53 70
     fetchQuestions,
71
+    insertNewSurveyResponse,
54 72
     updateSurveyByProfileId,
55 73
     scoreSurveyByProfileId,
56 74
     fetchResponsesByProfileId,

+ 1
- 0
frontend/src/services/user.service.js Ver arquivo

@@ -8,6 +8,7 @@ const signupUser = async user => {
8 8
     const payload = {
9 9
         user_name: user.name,
10 10
         user_email: user.email,
11
+        user_pass: user.password,
11 12
         is_poster: user.seeking == 'position' ? 0 : 1,
12 13
     }
13 14
     return await db.post(`/user/signup`, payload)

+ 1
- 1
frontend/src/utils/aspects.js Ver arquivo

@@ -32,4 +32,4 @@ const Aspects = [
32 32
     },
33 33
 ]
34 34
 
35
-export default Aspects
35
+export default Aspects

+ 35
- 11
frontend/src/utils/db.js Ver arquivo

@@ -27,36 +27,60 @@ class Connector {
27 27
             patch: 'PATCH',
28 28
         }
29 29
     }
30
-    _makeHeader({ method, payload }) {
30
+    _makeHeader({ method, payload, authorization }) {
31 31
         const header = { ...headerTemplate }
32 32
         header.method = method
33 33
         if (payload) {
34 34
             header.body = JSON.stringify(payload)
35 35
         }
36
+        if (authorization) {
37
+            header.headers.authorization = authorization
38
+        }
36 39
         return header
37 40
     }
38
-    async _tryFetch({ endpoint, header }) {
41
+    async _tryFetch({ endpoint, header }, returnHeaders = false) {
39 42
         try {
40 43
             const res = await fetch(`${remote}${endpoint}`, header)
44
+            const jsonRes = await res.json()
41 45
             if (!res.ok) {
42
-                throw Error(res.statusText)
46
+                // NOTE: Somewhat hacky workaround here to get auth working
47
+                if (res.status === 401) {
48
+                    return { status: res.status }
49
+                } else {
50
+                    throw Error(res.statusText)
51
+                }
52
+            }
53
+            if (returnHeaders) {
54
+                return res.headers
55
+            } else {
56
+                return jsonRes.data
43 57
             }
44
-            const jsonRes = await res.json()
45
-            return jsonRes.data
46 58
         } catch (error) {
47 59
             console.error(`[API Util]: ${error}\nroute:`, endpoint)
48 60
         }
49 61
     }
50
-    async get(endpoint) {
51
-        return await this._tryFetch({
52
-            endpoint,
53
-            header: this._makeHeader({ method: this._verbs.get }),
54
-        })
62
+    async get(endpoint, authHeaders = false) {
63
+        if (authHeaders) {
64
+            return await this._tryFetch({
65
+                endpoint,
66
+                header: this._makeHeader({
67
+                    method: this._verbs.get,
68
+                    authorization: `${authHeaders}`,
69
+                }),
70
+            })
71
+        } else {
72
+            return await this._tryFetch({
73
+                endpoint,
74
+                header: this._makeHeader({ method: this._verbs.get }),
75
+            })
76
+        }
55 77
     }
56
-    async post(endpoint, payload = {}) {
78
+    // TODO: probably needs authHeader param for insertSurveyResponses
79
+    async post(endpoint, payload = {}, returnHeaders = false) {
57 80
         return await this._tryFetch({
58 81
             endpoint,
59 82
             header: this._makeHeader({ method: this._verbs.post, payload }),
83
+            returnHeaders,
60 84
         })
61 85
     }
62 86
     async patch(endpoint, payload = {}) {

+ 110
- 41
frontend/src/utils/lang.js Ver arquivo

@@ -1,27 +1,87 @@
1
-const DELIMITER = '_'
1
+const aspectsArr = [1, 2, 3, 4, 5, 6]
2 2
 
3
-// TODO: Combine these two
3
+// Splash page is unique in survey steps and therefore is simply spliced in
4
+// during survey generation
5
+const splash = {
6
+    response_key_id: 20,
7
+    response_key_category: 'splash',
8
+    response_key_prompt: 'splash page',
9
+    response_key_description: 'required for splash page rendering',
10
+    aspect: null,
11
+    category: 'splash',
12
+    component: 'Splash',
13
+    survey_stage: 'splash',
14
+    placeholder: null,
15
+    invalidInputPrompt: null,
16
+}
17
+
18
+// Auth page is also unique in survey steps and is spliced in as well
19
+const auth = {
20
+    response_key_id: 21,
21
+    response_key_category: 'auth',
22
+    response_key_prompt: 'auth page',
23
+    response_key_description: 'required to authenticate user signup',
24
+    aspect: null,
25
+    category: 'auth',
26
+    component: 'Auth',
27
+    survey_stage: 'auth',
28
+    placeholder: null,
29
+    invalidInputPrompt: null,
30
+}
31
+
32
+// This has to be in order for survey (DO NOT TOUCH)
33
+const initialSteps = {
34
+    splash: 'splash',
35
+    email: 'email',
36
+    name: 'name',
37
+    seeking: 'seeking',
38
+    password: 'password',
39
+}
40
+
41
+// Easily reorder steps of survey here:
4 42
 const allSteps = {
5 43
     usa: {
6
-        splash: 'splash',
7
-        name: 'name',
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',
44
+        ...initialSteps,
45
+        aspect01: 'aspect-1',
46
+        aspect02: 'aspect-2',
47
+        aspect03: 'aspect-3',
18 48
         zipcode: 'zipcode',
19
-        // distance: 'distance',
20
-        blurb: 'blurb',
49
+        urgency: 'urgency',
50
+        aspect04: 'aspect-4',
51
+        aspect05: 'aspect-5',
52
+        aspect06: 'aspect-6',
53
+        presence: 'presence',
54
+        duration: 'duration',
55
+        pronouns: 'pronouns',
56
+        language: 'language',
21 57
         image: 'image',
22
-        aspects: 'aspects'
58
+        distance: 'distance',
59
+        blurb: 'blurb',
60
+        // experience: 'experience',
61
+        // roles: 'role',
23 62
     },
24 63
 }
64
+const surveyStages = {
65
+    7: allSteps.usa.name,
66
+    8: allSteps.usa.email,
67
+    9: allSteps.usa.password,
68
+    10: allSteps.usa.zipcode,
69
+    11: allSteps.usa.seeking,
70
+    12: allSteps.usa.image,
71
+    13: allSteps.usa.language,
72
+    14: allSteps.usa.duration,
73
+    15: allSteps.usa.presence,
74
+    16: allSteps.usa.blurb,
75
+    17: allSteps.usa.urgency,
76
+    18: allSteps.usa.pronouns,
77
+    19: allSteps.usa.distance,
78
+    1: allSteps.usa.aspect01,
79
+    2: allSteps.usa.aspect02,
80
+    3: allSteps.usa.aspect03,
81
+    4: allSteps.usa.aspect04,
82
+    5: allSteps.usa.aspect05,
83
+    6: allSteps.usa.aspect06,
84
+}
25 85
 
26 86
 const aspectResponses = {
27 87
     usa: {
@@ -52,33 +112,42 @@ possible.usa = {
52 112
     // key 10
53 113
     duration: ['full-time', 'part-time', 'contract', 'flexible'],
54 114
     // Experience and roles concat, key: 14
55
-    experience: ['associate', 'junior', 'mid-level', 'senior', 'staff'],
56
-    roles: {
57
-        type: [
58
-            'back-end',
59
-            'database',
60
-            'front-end',
61
-            'full-stack',
62
-            'qa',
63
-            'security',
64
-            'system',
65
-            'test',
66
-        ],
67
-        position: [
68
-            'administrator',
69
-            'analyst',
70
-            'architect',
71
-            'developer',
72
-            'engineer',
73
-            'manager',
74
-            'technician',
75
-        ],
76
-        candidate: ['hiring_manager', 'recruiter'],
77
-    },
115
+    //     experience: ['associate', 'junior', 'mid-level', 'senior', 'staff'],
116
+    // roles: {
117
+    // type: [
118
+    // 'back-end',
119
+    // 'database',
120
+    // 'front-end',
121
+    // 'full-stack',
122
+    // 'qa',
123
+    // 'security',
124
+    // 'system',
125
+    // 'test',
126
+    // ],
127
+    // position: [
128
+    // 'administrator',
129
+    // 'analyst',
130
+    // 'architect',
131
+    // 'developer',
132
+    // 'engineer',
133
+    // 'manager',
134
+    // 'technician',
135
+    // ],
136
+    // candidate: ['hiring_manager', 'recruiter'],
137
+    // },
78 138
     pronouns: ['she/her', 'she/they', 'he/him', 'he/they', 'they/them'],
139
+    //    role: ['role1', 'role2'],
79 140
     image: [],
80 141
     zipcode: [],
81 142
     blurb: [],
82 143
 }
83 144
 
84
-export { allSteps, aspectResponses, possible, DELIMITER }
145
+export {
146
+    allSteps,
147
+    splash,
148
+    auth,
149
+    surveyStages,
150
+    aspectResponses,
151
+    aspectsArr,
152
+    possible,
153
+}

+ 92
- 66
frontend/src/utils/survey.js Ver arquivo

@@ -1,76 +1,91 @@
1 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
-}
2
+import { fetchQuestions, insertNewSurveyResponse } from '../services/index.js'
3
+import { splash, auth, possible, surveyStages, allSteps } from './lang.js'
50 4
 
51 5
 class SurveyFactory {
52 6
     constructor() {
53 7
         this.questionsFromDb = []
8
+        this.responsesFromDb = []
9
+    }
10
+    _addResponses(responseKeys, responsesByCategory) {
11
+        const existingResponses = {}
12
+        // Removes empty form drop down options from possible['usa']
13
+        Object.keys(responsesByCategory).forEach(categoryKey => {
14
+            if (responsesByCategory[categoryKey].length) {
15
+                existingResponses[categoryKey] =
16
+                    responsesByCategory[categoryKey]
17
+            }
18
+        })
19
+        // Adds form drop down options to each responseKey
20
+        Object.keys(existingResponses).forEach(inputKey => {
21
+            responseKeys.forEach(responseKey => {
22
+                if (responseKey.survey_stage == inputKey) {
23
+                    responseKey.responses = existingResponses[inputKey]
24
+                }
25
+            })
26
+        })
27
+        return responseKeys
54 28
     }
55
-    _setSteps(langFile) {
56
-        const stepsToProcess = [...Object.values(langFile)]
57
-        const seenIds = []
58
-        const stepsInCommon = stepsToProcess.map(step => {
59
-            // Match question to step
60
-            const match = hasMatch(step, this.questionsFromDb)
61
-            if (match) {
62
-                seenIds.push(match.response_key_id)
29
+    _addComponents(responseKeys) {
30
+        responseKeys.forEach(responseKey => {
31
+            switch (responseKey.category) {
32
+                case 'input':
33
+                    responseKey.component = 'FormInput'
34
+                    break
35
+                case 'choice':
36
+                    responseKey.component = 'FormDropdown'
37
+                    break
38
+                case 'aspect':
39
+                    responseKey.component = 'QuestionResponse'
40
+                    break
63 41
             }
64
-            const responseKeyLike = formatStep(match, step)
65
-            const withComponent = associateWithComponent(responseKeyLike)
66
-            console.log('withComponent :>> ', withComponent)
67
-            return withComponent
68 42
         })
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),
43
+        return responseKeys
44
+    }
45
+    _addSurveySteps(responseKeys, surveyStages) {
46
+        responseKeys.forEach(responseKey => {
47
+            Object.keys(surveyStages).forEach((stage, i) => {
48
+                if (responseKey.response_key_id == stage) {
49
+                    responseKey.survey_stage = surveyStages[i + 1]
50
+                }
51
+            })
52
+        })
53
+        return responseKeys
54
+    }
55
+    // TODO: Don't nest the for loop...
56
+    _sortSurveySteps(mutatedResponseKeys, allSteps) {
57
+        const reordered = []
58
+        Object.values(allSteps).forEach(step => {
59
+            Object.values(mutatedResponseKeys).forEach(response => {
60
+                if (surveyStages[response.response_key_id] === step) {
61
+                    response.survey_stage = step
62
+                    reordered.push(response)
63
+                }
64
+            })
65
+        })
66
+        return reordered
67
+    }
68
+    _setSteps() {
69
+        const responseKeys = this.questionsFromDb
70
+        const responsesByCategory = possible['usa']
71
+        let mutatedResponseKeys = this._addSurveySteps(
72
+            responseKeys,
73
+            surveyStages,
74
+        )
75
+        mutatedResponseKeys = this._addResponses(
76
+            mutatedResponseKeys,
77
+            responsesByCategory,
78
+        )
79
+        mutatedResponseKeys = this._addComponents(responseKeys)
80
+        mutatedResponseKeys = this._sortSurveySteps(
81
+            mutatedResponseKeys,
82
+            allSteps['usa'],
72 83
         )
73
-        return [...stepsInCommon, ...unseen]
84
+        // Splash page is placed at beginning of survey
85
+        mutatedResponseKeys.unshift(splash)
86
+        // Auth page is placed after email/password
87
+        mutatedResponseKeys.splice(5, 0, auth)
88
+        return mutatedResponseKeys
74 89
     }
75 90
     async getQuestions() {
76 91
         try {
@@ -80,14 +95,25 @@ class SurveyFactory {
80 95
             console.error(err)
81 96
         }
82 97
     }
83
-    async createSurvey(langFile, roleTree) {
98
+    async addNewSurveyAnswer(responses, profileId) {
99
+        try {
100
+            this.responsesFromDb = await insertNewSurveyResponse(
101
+                responses,
102
+                profileId,
103
+            )
104
+            return this.responsesFromDb
105
+        } catch (err) {
106
+            console.error(err)
107
+        }
108
+    }
109
+    async createSurvey(roleTree) {
84 110
         if (!this.questionsFromDb.length) {
85 111
             const res = await this.getQuestions()
86 112
             console.warn(
87 113
                 `Attempted to create a survey before getting questions: retrieved ${res.length} questions`,
88 114
             )
89 115
         }
90
-        const steps = this._setSteps(langFile)
116
+        const steps = this._setSteps()
91 117
         return new Survey(steps, roleTree)
92 118
     }
93 119
 }

+ 44
- 8
frontend/src/views/ChatView.vue Ver arquivo

@@ -2,8 +2,13 @@
2 2
 main.view--chat
3 3
     header.mb6(v-if='profile && grouping')
4 4
         h3 chatting with:
5
-        p {{ target.profile_id }} | {{ grouping.profile.user_name }} | {{ grouping.profile.user_email }}
6
-
5
+        span {{ target.profile_id }} |
6
+        span(v-if='grouping.revealedFromNotification.length')
7
+            span(v-for='revealed in grouping.revealedFromNotification')
8
+                span(v-if='revealed.profile_id === target.profile_id')
9
+                    span {{ revealed[revealed.tag_description] }} |
10
+        span(v-else)
11
+            span {{ grouping.profile.user_name }} | {{ grouping.profile.user_email }}
7 12
         h3 logged in as:
8 13
         p {{ profile.id }} | {{ profile._profile.user_name }} | {{ profile._profile.user_email }}
9 14
         //- p subscriptions: {{ profile.chatter.subscriptions }}
@@ -13,10 +18,24 @@ main.view--chat
13 18
             .w-flex.row
14 19
                 button(@click='reveal(7)') reveal my name
15 20
                 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] }}
21
+                // TODO: Remove later, only for testing
22
+                button(@click='checkData()') check data
23
+            span(v-if='grouping.revealed[profile.id.value]')
24
+                p you revealed:
25
+                ul(
26
+                    v-for='reveal in [...new Set(grouping.revealed[profile.id.value])]'
27
+                )
28
+                    li {{ reveal.description }}
29
+            span(v-if='grouping.revealed[target.profile_id]')
30
+                p they revealed:
31
+                ul(v-for='reveal in grouping.revealed[target.profile_id]')
32
+                    li {{ reveal.description }}: {{ target[reveal.description] }}
33
+            span(v-if='grouping.revealedFromNotification.length')
34
+                p recently revealed:
35
+                ul(v-for='revealed in grouping.revealedFromNotification')
36
+                    li(
37
+                        v-if='revealed[revealed.tag_description] !== profile._profile[revealed.tag_description]'
38
+                    ) {{ revealed.tag_description }}: {{ revealed[revealed.tag_description] }}
20 39
 
21 40
     article
22 41
         template(v-if='isLoading')
@@ -41,7 +60,7 @@ main.view--chat
41 60
     footer.w-flex.row
42 61
         w-input(@keyup.enter='sendMessage' outline v-model='toSend')
43 62
         w-button(@click='sendMessage')
44
-            w-icon mdi mdi-send
63
+            w-icon.icon-paper-plane
45 64
 
46 65
     MainNav
47 66
 </template>
@@ -59,8 +78,14 @@ export default {
59 78
         target: null,
60 79
         toSend: '',
61 80
         messages: [],
62
-        grouping: null,
81
+        grouping: {},
82
+        them: {},
63 83
     }),
84
+    computed: {
85
+        theyRevealed() {
86
+            return this.them
87
+        },
88
+    },
64 89
     watch: {
65 90
         async profile() {
66 91
             this.loadTargetProfile()
@@ -74,6 +99,7 @@ export default {
74 99
         this.loadTargetProfile()
75 100
         currentProfile._loading.value = true
76 101
         this.grouping = this.getGrouping()
102
+        this.them = this.grouping.profile
77 103
         this.messages = await currentProfile.chatter.getHistory(this.grouping.grouping_name)
78 104
         console.log('this.messages :>> ', this.messages)
79 105
         currentProfile.chatter.setOnMessage(this._onMessage)
@@ -83,6 +109,13 @@ export default {
83 109
         async reveal(tagId) {
84 110
             const grouping = this.getGrouping()
85 111
             await grouping.reveal(currentProfile.id.value, tagId)
112
+         },
113
+        // TODO: remove, only for testing reveal()
114
+        async checkData() {
115
+            console.log('currentProfile :=>', currentProfile)
116
+            // TODO: remove once Vue Router guards allow to redirect via url
117
+            console.log('currentProfile.isComplete :=>', currentProfile.isComplete) // false
118
+            console.log('currentProfile.isLoggedIn :=>', currentProfile.isLoggedIn) // true
86 119
         },
87 120
         /**
88 121
          * Pubnub message callback fires when message event
@@ -95,6 +128,9 @@ export default {
95 128
         },
96 129
         // TODO: test this
97 130
         getGrouping() {
131
+            if (this.$route.params.pid === currentProfile.profile_id) {
132
+                console.warn('WARNING :=> You cannot chat with yourself!!!')
133
+            }
98 134
             return currentProfile.groupings.find(
99 135
                 g => g.profile.profile_id == this.$route.params.pid,
100 136
             )

+ 28
- 4
frontend/src/views/HomeView.vue Ver arquivo

@@ -1,11 +1,11 @@
1 1
 <template lang="pug">
2 2
 main.view--home
3
-    article.w-flex.column.align-center
3
+    article.w-flex.sm-column.md-row.align-center
4 4
         template(v-if='isLoading')
5 5
             w-spinner(bounce)
6 6
 
7 7
         template(v-else-if='!isLoading && cards.length > 0')
8
-            ProfileCardList(:cards='cards')
8
+            ProfileCardList(:cards='cards' @loadMore='onLoadMore')
9 9
 
10 10
         template(v-else-if='cards.length === 0')
11 11
             p No profiles in match_queue.
@@ -21,7 +21,7 @@ import PairingButton from '../components/PairingButton.vue'
21 21
 
22 22
 import { Card } from '../entities'
23 23
 
24
-import { currentProfile } from '../services'
24
+import { currentProfile, fetchQueueByProfileId } from '../services'
25 25
 import { mixins } from '../utils'
26 26
 
27 27
 const notificationOpts = {
@@ -60,12 +60,36 @@ export default {
60 60
         PairingButton,
61 61
     },
62 62
     mixins: [mixins.profileMixin],
63
+    data() {
64
+        return {
65
+            fetchedCards: [],
66
+            offset: 0,
67
+        }
68
+    },
63 69
     computed: {
70
+        cP() {
71
+            return currentProfile ? currentProfile : null
72
+        },
64 73
         cards() {
65
-            return currentProfile.queue.map(qProfile => convertToCard(qProfile))
74
+            let initialCards = currentProfile.queue.map(qProfile =>
75
+                convertToCard(qProfile),
76
+            )
77
+            if (this.fetchedCards.length === 0) return initialCards
78
+            return [
79
+                ...initialCards,
80
+                ...this.fetchedCards.map(qProfile => convertToCard(qProfile)),
81
+            ]
66 82
         },
67 83
     },
68 84
     methods: {
85
+        async onLoadMore() {
86
+            this.offset += 5 // fetch next batch with updated offset
87
+            let newQueue = await fetchQueueByProfileId(
88
+                currentProfile.id._value,
89
+                this.offset,
90
+            )
91
+            this.fetchedCards.push(...newQueue) // update fetchedCards => recalculate cards
92
+        },
69 93
         // this can be placed in utils/notification.js
70 94
         notify(payload) {
71 95
             notificationOpts.message = payload

+ 2
- 2
frontend/src/views/LoginView.vue Ver arquivo

@@ -3,8 +3,8 @@ main.view--login
3 3
 
4 4
     article.pa12
5 5
         form
6
-            w-input.mb4(label="User E-mail" tile outline v-model="form.profileId" inner-icon-left='mdi mdi-mail')
7
-            w-input(label="Password" type="password" tile outline inner-icon-left='mdi mdi-eye-off')
6
+            w-input.mb4(label="User E-mail" tile outline v-model="form.profileId" inner-icon-left='icon-envelope')
7
+            w-input(label="Password" type="password" tile outline inner-icon-left='icon-eye')
8 8
 
9 9
             //- Emit up an event so we can sync App pid with currentProfile.id
10 10
             w-button.xs12.mt12(@click="$emit('updatePid', form.profileId)" type="submit") submit

+ 105
- 15
frontend/src/views/OnboardingView.vue Ver arquivo

@@ -2,57 +2,144 @@
2 2
 main.view--onboarding
3 3
     article(
4 4
         style='display: flex; flex-direction: column; align-items: center'
5
-        v-if='survey'
5
+        v-if='currentStep !== survey?.steps?.length'
6 6
     )
7
-        .step(v-for='(step, i) in survey.steps')
7
+        .answers(v-for='(value, key) in answered')
8
+            span(v-if='key == "name" && value && currentStep == 2') Hi {{ value }}!
9
+        br
10
+        .step(v-for='(step, i) in survey?.steps')
8 11
             component(
9
-                :aspect-questions='step.component == "Aspects" ? survey.aspectQuestions : null'
12
+                :answered='answered'
13
+                :currentStep='currentStep'
10 14
                 :is='step.component'
11 15
                 :question='step'
16
+                :responses='responses'
17
+                :survey='survey'
18
+                :surveyStepsCount='survey?.steps?.length'
12 19
                 @handle-submit='onSubmit'
13 20
                 @update-answers='updateAnswers'
14 21
                 v-if='step && currentStep == i'
15 22
             )
23
+        .invalidResponseMessage(
24
+            style='text-align: center'
25
+            v-if='invalidResponse'
26
+        )
27
+            p {{ survey.steps[currentStep].invalidInputPrompt }}
28
+
29
+        footer
30
+            p(v-if='currentStep != 0') You have completed: {{ currentStep }} / {{ survey?.steps?.length }} survey steps
31
+
32
+    article(v-else)
33
+        SurveyCompleteView(:answers='answered' :surveySteps='survey?.steps')
16 34
 </template>
17 35
 
18 36
 <script>
37
+import { Authenticator } from '../services/auth.service.js'
19 38
 import { surveyFactory } from '@/utils'
20
-import { allSteps } from '@/utils/lang'
21 39
 import stepViews from '@/components/onboarding'
40
+import SurveyCompleteView from './SurveyCompleteView.vue'
41
+let hashedAccessToken = null
42
+let currentProfileId = null
22 43
 
23
-// import savesurveybyprfileid - call it on submit
24
-// paginate to save every steps answers
25 44
 export default {
26 45
     name: 'OnboardingView',
27 46
     components: {
28 47
         ...stepViews,
48
+        SurveyCompleteView,
29 49
     },
30 50
     data: () => ({
31 51
         answered: {},
32 52
         aspectQuestions: [],
53
+        responses: [],
33 54
         currentStep: 0,
34 55
         survey: null,
56
+        invalidResponse: false,
57
+        authenticator: {},
35 58
     }),
36 59
     async created() {
37
-        this.survey = await surveyFactory.createSurvey(allSteps['usa'])
60
+        this.survey = await surveyFactory.createSurvey()
61
+        this.authenticator = new Authenticator()
62
+        hashedAccessToken = this.grabStoredCookie('siimee_access')
63
+        try {
64
+            const sessionData = await this.verifySession(hashedAccessToken)
65
+            currentProfileId = sessionData.profileId
66
+            this.responses = sessionData.responses
67
+            this.currentStep = this.responses.length + 3
68
+            this.goToStep(this.currentStep)
69
+        } catch (err) {
70
+            console.error('ERROR :=>', err)
71
+            this.goToStep(0)
72
+        }
38 73
     },
39 74
     methods: {
40 75
         onSubmit() {
41 76
             console.log(JSON.stringify(this.answered))
42 77
         },
43
-        goToStep(num) {
78
+        async goToStep(num) {
44 79
             this.currentStep = num
45 80
         },
46
-        updateAnswers(payload) {
47
-            // null payload is passed on splash page
81
+        grabStoredCookie(cookieKey) {
82
+            const cookies = document.cookie
83
+                .split('; ')
84
+                .reduce((prev, current) => {
85
+                    const [name, ...value] = current.split('=')
86
+                    prev[name] = value.join('=')
87
+                    return prev
88
+                }, {})
89
+            const cookieVal =
90
+                cookieKey in cookies ? cookies[`${cookieKey}`] : undefined
91
+            return cookieVal
92
+        },
93
+        async verifySession(hashedAccessToken) {
94
+            if (!hashedAccessToken)
95
+                return console.warn('WARNING :=> accessToken is not defined')
96
+            const validatedToken = await this.authenticator.validateSession(
97
+                hashedAccessToken,
98
+            )
99
+            if (validatedToken.error) {
100
+                throw new Error(validatedToken.error)
101
+            } else {
102
+                return validatedToken
103
+            }
104
+        },
105
+        async updateAnswers(payload) {
48 106
             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)}`)
107
+                const k = payload.question.survey_stage
108
+                this.answered[k] = payload.input
109
+                // Once validated, don't log password in answered object
110
+                this.answered[k] = k === 'password' ? undefined : payload.input
111
+                // Hacky WorkAround for Validating Answers
112
+                if (!this.survey.validateAnswer(payload)) {
113
+                    this.invalidResponse = true
114
+                    return
115
+                }
116
+                // Formats initial responses for response table
117
+                const response = {}
118
+                response.response_key_id = payload.question.response_key_id
119
+                response.val = payload.input
120
+                this.responses.push(response)
53 121
                 if (k === 'aspects') return
54 122
             }
55
-            this.goToStep(this.currentStep + 1)
123
+            // NOTE: If user has finished minimum profile creation,
124
+            // Adds survey answers to responses table and verifies tokens on each step
125
+            if (currentProfileId) {
126
+                await surveyFactory.addNewSurveyAnswer(
127
+                    this.responses[this.responses.length - 1],
128
+                    currentProfileId,
129
+                )
130
+                try {
131
+                    await this.verifySession(hashedAccessToken)
132
+                } catch (err) {
133
+                    this.currentStep = 0
134
+                    this.goToStep(this.currentStep)
135
+                    throw new Error(err)
136
+                }
137
+            }
138
+            if (this.currentStep > this.survey.steps.length) {
139
+                this.onSubmit(this.answered)
140
+            } else {
141
+                this.goToStep(this.currentStep + 1)
142
+            }
56 143
         },
57 144
     },
58 145
 }
@@ -70,6 +157,9 @@ export default {
70 157
     article
71 158
         height: 100vh
72 159
 
160
+    .answers
161
+        text-align: center
162
+
73 163
     .w-button
74 164
             display: flex
75 165
             width: 315px

+ 1
- 1
frontend/src/views/PairsView.vue Ver arquivo

@@ -10,7 +10,7 @@ main.view--pairs
10 10
                 //- pending tab
11 11
                 template(#item-content.1='{ item }')
12 12
                     PairsList(:pairs='pending' tab-name='pending')
13
-                //- paired tab 
13
+                //- paired tab
14 14
                 template(#item-content.2='{ item }')
15 15
                     PairsList(:pairs='paired' tab-name='paired')
16 16
 

+ 62
- 0
frontend/src/views/SurveyCompleteView.vue Ver arquivo

@@ -0,0 +1,62 @@
1
+<template lang="pug">
2
+main.view--surveycomplete
3
+    article(style='display: flex; flex-direction: column; align-items: center; text-align: center;')
4
+        h2 Thanks for Completing Our Survey!!
5
+        h1 Please review your answers and let us know if you need to change anything.
6
+        br
7
+        p(v-for='input in formInputs')
8
+            p(v-for='(value, key) in answers')
9
+                p(v-if='input.survey_stage == key && key !== "password"')
10
+                    p Your {{ key }}: {{ value }}
11
+        br
12
+        p(v-for='input in formDropdowns')
13
+            p(v-for='(value, key) in answers')
14
+                p(v-if='input.survey_stage == key')
15
+                    p Your {{ key }}: {{ value }}
16
+        br
17
+        p(v-for='(response, responseIndex) in questionResponses')
18
+            p(v-for='(value, key) in answers')
19
+                p(v-if='response.survey_stage == key') 
20
+                    p Survey Question {{ responseIndex + 1 }}: 
21
+                    p {{ response.response_key_prompt }}
22
+                    p You Answered: {{ value }}
23
+                    br
24
+        w-button.ma1(@click="changeAnswers") Change Answers
25
+        w-button.ma1(@click="finalSubmit") Submit Answers
26
+</template>
27
+
28
+<script>
29
+export default {
30
+    props: {
31
+        answers: {
32
+            type: Object,
33
+            default: () => ({}),
34
+        },
35
+        surveySteps: {
36
+            type: Array,
37
+            default: () => [],
38
+        },
39
+    },
40
+    data: () => ({
41
+        surveyObjects: [],
42
+        formInputs: [],
43
+        questionResponses: [],
44
+        formDropdowns: [],
45
+    }),
46
+    created() {
47
+        this.surveySteps.forEach((step) => {
48
+            switch (step.component) {
49
+                case 'FormInput':
50
+                    this.formInputs.push(step)
51
+                    break
52
+                case 'FormDropdown':
53
+                    this.formDropdowns.push(step)
54
+                    break
55
+                case 'QuestionResponse':
56
+                    this.questionResponses.push(step)
57
+                    break
58
+            }
59
+        })
60
+    },
61
+}
62
+</script>

+ 27
- 4
frontend/src/views/SurveyView.vue Ver arquivo

@@ -30,7 +30,7 @@ main.view--survey.f-col.start.w-full
30 30
                                 @input='storeResponseLike(step, q.response_key_id, q.response_key_prompt, profile[q.response_key_prompt])'
31 31
                                 @keyup.enter='step++'
32 32
                                 v-model='profile[q.response_key_prompt]'
33
-                            ) 
33
+                            )
34 34
                             label >{{ profile[q.response_key_prompt] }}
35 35
 
36 36
                         //- Aspects
@@ -85,7 +85,7 @@ import {
85 85
     signupUser,
86 86
 } from '@/services'
87 87
 
88
-import { maxDistanceKey } from '../../../backend/db/data-generator/config.json'
88
+import { distanceKey } from '../../../backend/db/data-generator/config.json'
89 89
 
90 90
 export default {
91 91
     props: {
@@ -207,7 +207,13 @@ export default {
207 207
 
208 208
             return userProfileRel
209 209
         },
210
-        async _getProfileWithScore(createdProfileId, maxDistance) {
210
+        async _getProfileWithScore(
211
+            createdProfileId,
212
+            maxDistance,
213
+            duration,
214
+            presence,
215
+            certifications,
216
+        ) {
211 217
             /** A Profile is associated with n:1 user and referenced by profile id */
212 218
             const fetchedProfile = await fetchProfileByProfileId(
213 219
                 createdProfileId,
@@ -223,6 +229,9 @@ export default {
223 229
             const scored = await scoreSurveyByProfileId(
224 230
                 createdProfileId,
225 231
                 maxDistance,
232
+                duration,
233
+                presence,
234
+                certifications,
226 235
             )
227 236
             if (!scored) {
228 237
                 console.error(`Could not score Profile ${createdProfileId}.`)
@@ -248,8 +257,19 @@ export default {
248 257
                 Object.values(this.responseLikes),
249 258
             )
250 259
             const maxDistanceRes = survey.find(
251
-                res => res.response_key_id == maxDistanceKey,
260
+                res => res.response_key_id == distanceKey,
261
+            )
262
+            const duration = survey.find(
263
+                res => res.response_key_id == prefKeys[1], // == 10
264
+            )
265
+            const presence = survey.find(
266
+                res => (res.response_key_id = prefKeys[2]), // == 11
252 267
             )
268
+            // TODO find certifications in responses
269
+            const certifications = survey.find(
270
+                res => (res.response_key_id = certifications),
271
+            )
272
+
253 273
             /**
254 274
              * Creating a profile only returns the created
255 275
              * user id and profile id
@@ -264,6 +284,9 @@ export default {
264 284
             const fetchedProfile = await this._getProfileWithScore(
265 285
                 createdProfileId,
266 286
                 maxDistanceRes.val,
287
+                duration.val,
288
+                presence.val,
289
+                certifications.val,
267 290
             )
268 291
 
269 292
             /**

+ 77
- 0
frontend/src/views/VerifyView.vue Ver arquivo

@@ -0,0 +1,77 @@
1
+<template lang="pug">
2
+.wait-message
3
+    p.verify-message Thanks for authenticating your email!
4
+    p.verify-message Please give us a moment to redirect you back to the survey
5
+</template>
6
+
7
+<script>
8
+import { Authenticator } from '../services/auth.service.js'
9
+let hash = null
10
+let hashedAccessToken = null
11
+export default {
12
+    name: 'VerifyView',
13
+    data: () => ({
14
+        authenticator: {},
15
+    }),
16
+    async created() {
17
+        this.authenticator = new Authenticator()
18
+        hash = this.$route.params.hashedToken
19
+        hashedAccessToken = this.grabCookie('siimee_access')
20
+        try {
21
+            this.isHashInUrl(hash)
22
+            await this.doesAccessTokenExist(hashedAccessToken)
23
+            await this.verifyActiveSession(hash)
24
+            await this.isSessionTokenValid(hash)
25
+        } catch (err) {
26
+            console.error(err)
27
+        }
28
+        this.$router.push('/onboarding')
29
+    },
30
+    methods: {
31
+        grabCookie(cookieKey) {
32
+            const cookies = document.cookie
33
+                .split('; ')
34
+                .reduce((prev, current) => {
35
+                    const [name, ...value] = current.split('=')
36
+                    prev[name] = value.join('=')
37
+                    return prev
38
+                }, {})
39
+            return `${cookieKey}` in cookies
40
+                ? cookies[`${cookieKey}`]
41
+                : undefined
42
+        },
43
+        isHashInUrl(hash) {
44
+            if (!hash) throw new Error('URL contains no hash!')
45
+        },
46
+        async doesAccessTokenExist(hashedAccessToken) {
47
+            if (!hashedAccessToken)
48
+                throw new Error('accessToken not in cookie store!')
49
+        },
50
+        async verifyActiveSession(hashedToken) {
51
+            const sessionData = await this.authenticator.verifyAuthSession(
52
+                hashedToken,
53
+            )
54
+            if (!sessionData.hashesMatch)
55
+                throw new Error('Hash is not in activeSessions!')
56
+        },
57
+        async isSessionTokenValid(hash) {
58
+            const sessionTokenIsValid =
59
+                await this.authenticator.validateSession(hash)
60
+            if (sessionTokenIsValid.error) {
61
+                throw new Error(sessionTokenIsValid.error)
62
+            }
63
+        },
64
+    },
65
+}
66
+</script>
67
+
68
+<style>
69
+.wait-message {
70
+    margin: 5rem auto;
71
+    text-align: center;
72
+    width: 90%;
73
+    max-width: 35rem;
74
+    font-size: 150%;
75
+    font-weight: bold;
76
+}
77
+</style>

Carregando…
Cancelar
Salvar