Ver código fonte

Merge branch 'brian_dev_141' of fyindr/siimee into dev

tags/0.0.3^2
maeda 3 anos atrás
pai
commit
2664b39d35

+ 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 10:
142
+                        resToEdit.val = random.duration()
143
+                        break
144
+                    case 11:
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

+ 120
- 36
backend/db/data-generator/mock.js Ver arquivo

@@ -430,108 +430,192 @@ module.exports = {
430 430
         {
431 431
             response_key_id: 1,
432 432
             response_key_category: 'visionary_vs_implementer',
433
-            response_key_prompt:
434
-                '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?',
435 434
             response_key_description: 'first round draft scoring question',
435
+            aspect: 'visionary_vs_implementer',
436
+            category: 'aspect',
437
+            placeholder: null,
438
+            invalidInputPrompt: null,
436 439
         },
437 440
         {
438 441
             response_key_id: 2,
439 442
             response_key_category: 'creative_vs_methodical',
440
-            response_key_prompt:
441
-                '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?',
442 444
             response_key_description: 'first round draft scoring question',
445
+            aspect: 'creative_vs_methodical',
446
+            category: 'aspect',
447
+            placeholder: null,
448
+            invalidInputPrompt: null,
443 449
         },
444 450
         {
445 451
             response_key_id: 3,
446 452
             response_key_category: 'dynamic_vs_ordered',
447
-            response_key_prompt:
448
-                'Do you structure and encourage your team to be a Collaborative or Independent environment? (NEEDS UPDATED COPY)',
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?',
449 454
             response_key_description: 'first round draft scoring question',
455
+            aspect: 'dynamic_vs_ordered',
456
+            category: 'aspect',
457
+            placeholder: null,
458
+            invalidInputPrompt: null,
450 459
         },
451 460
         {
452 461
             response_key_id: 4,
453 462
             response_key_category: 'precise_vs_resourceful',
454
-            response_key_prompt:
455
-                'Do you find higher success in employees on your team that are Innovative or Conventional? (NEEDS UPDATED COPY)',
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?',
456 464
             response_key_description: 'first round draft scoring question',
465
+            aspect: 'precise_vs_resourceful',
466
+            category: 'aspect',
467
+            placeholder: null,
468
+            invalidInputPrompt: null,
457 469
         },
458 470
         {
459 471
             response_key_id: 5,
460 472
             response_key_category: 'big_Picture_vs_focused',
461
-            response_key_prompt:
462
-                '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?',
463 474
             response_key_description: 'first round draft scoring question',
475
+            aspect: 'big_Picture_vs_focused',
476
+            category: 'aspect',
477
+            placeholder: null,
478
+            invalidInputPrompt: null,
464 479
         },
465 480
         {
466 481
             response_key_id: 6,
467 482
             response_key_category: 'guided_vs_self-managed',
468
-            response_key_prompt:
469
-                '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?',
470 484
             response_key_description: 'first round draft scoring question',
485
+            aspect: 'guided_vs_self-managed',
486
+            category: 'aspect',
487
+            placeholder: null,
488
+            invalidInputPrompt: null,
471 489
         },
472 490
         {
473 491
             response_key_id: 7,
474 492
             response_key_category: 'profile',
475
-            response_key_prompt: 'zipcode',
476
-            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?',
477 499
         },
478 500
         {
479 501
             response_key_id: 8,
480 502
             response_key_category: 'profile',
481
-            response_key_prompt: 'image',
482
-            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',
483 509
         },
484 510
         {
485 511
             response_key_id: 9,
486 512
             response_key_category: 'profile',
487
-            response_key_prompt: 'language',
488
-            response_key_description:
489
-                '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.',
490 519
         },
491 520
         {
492 521
             response_key_id: 10,
493 522
             response_key_category: 'profile',
494
-            response_key_prompt: 'duration',
495
-            response_key_description:
496
-                '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',
497 529
         },
498 530
         {
499 531
             response_key_id: 11,
500 532
             response_key_category: 'profile',
501
-            response_key_prompt: 'presence',
502
-            response_key_description:
503
-                '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.',
504 539
         },
505 540
         {
506 541
             response_key_id: 12,
507 542
             response_key_category: 'profile',
508
-            response_key_prompt: 'blurb',
509
-            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.',
510 549
         },
511 550
         {
512 551
             response_key_id: 13,
513 552
             response_key_category: 'profile',
514
-            response_key_prompt: 'urgency',
515
-            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?',
516 559
         },
517 560
         {
518 561
             response_key_id: 14,
519 562
             response_key_category: 'profile',
520
-            response_key_prompt: 'role',
521
-            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!',
522 569
         },
523 570
         {
524 571
             response_key_id: 15,
525 572
             response_key_category: 'profile',
526
-            response_key_prompt: 'pronouns',
527
-            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.',
528 579
         },
529 580
         {
530 581
             response_key_id: 16,
531 582
             response_key_category: 'profile',
532
-            response_key_prompt: 'distance',
533
-            response_key_description:
534
-                '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.',
535 619
         },
536 620
     ],
537 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
 

+ 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 = {

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


+ 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>

+ 12
- 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,24 @@ 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 = this.question.response_key_prompt.split('[break]')
32
+        this.parsedPrompt.start = parsedPromptArr[0]
33
+        this.parsedPrompt.mid = parsedPromptArr[1]
34
+        this.parsedPrompt.end = parsedPromptArr[2]
35
+    },
27 36
     methods: {
28 37
         handleSubmit() {
29
-            if (!this.selection) {
30
-                console.warn('Please select a role.')
31
-                return
32
-            }
33 38
             let payload = {
34 39
                 question: this.question,
35
-                answer: this.selection,
40
+                input: this.selection,
36 41
             }
37 42
             this.$emit('update-answers', payload)
38 43
         },

+ 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-if='question.survey_stage !== "image" && question.survey_stage !== "blurb" && question.survey_stage !== "password"'
12
+        v-model='input'
13
+        v-focus
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-model='input'
22
+        v-focus
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='handlSubmit'
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
+        currentSteps: {
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
 }

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

@@ -6,10 +6,10 @@ import Skills from './Skills.vue'
6 6
 import Location from './Location.vue'
7 7
 import Interests from './Interests.vue'
8 8
 import LicensesAndCertifications from './LicensesAndCertifications.vue'
9
-import Aspects from './Aspects.vue'
10 9
 import FormInput from './FormInput.vue'
11 10
 import FormTags from './FormTags.vue'
12 11
 import FormDropdown from './FormDropdown.vue'
12
+import QuestionResponse from './QuestionResponse.vue'
13 13
 
14 14
 export default {
15 15
     Splash,
@@ -20,8 +20,8 @@ export default {
20 20
     Location,
21 21
     Interests,
22 22
     LicensesAndCertifications,
23
-    Aspects,
24 23
     FormDropdown,
25 24
     FormTags,
26 25
     FormInput,
26
+    QuestionResponse,
27 27
 }

+ 2
- 2
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.'
@@ -133,8 +134,7 @@ const makeCardFromProfile = profile => {
133 134
     c.locale = `${profile.city}, ${profile.state}`
134 135
     c.email = profile.user_email
135 136
 
136
-
137
-    let aspectResponses = profile?.responses.filter(r => [1,2,3,4,5,6].indexOf(r.response_key_id) !== -1)
137
+    let aspectResponses = profile?.responses.filter(r => aspectsArr.indexOf(r.response_key_id) !== -1)
138 138
     if(aspectResponses.length){ // if user has responses for aspects we overwrite default percentages
139 139
         c.aspects.map(a => {
140 140
             a.percentage = Number(aspectResponses.find(r => responseKeyIdToAspectName[r.response_key_id] == a.name).val )

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

@@ -0,0 +1,40 @@
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
+    // Comment out and uncomment below for more robust password testing
11
+    password: Joi.string().min(14).max(30),
12
+    // TODO: consider using a more robust library for password validation
13
+    // password: Joi.string()
14
+        // .min(14)
15
+        // .max(30)
16
+        // .pattern(
17
+            // new RegExp(
18
+                // '^(?=.*[!@#$%^&*()_+\\-=[\\]{};\':"\\\\|,.<>\\/?])(?=.*[!@#$%^&*()_+\\-=[\\]{};\':"\\\\|,.<>\\/?])[a-zA-Z0-9!@#$%^&*()_+\\-=[\\]{};\':"\\\\|,.<>\\/?]{14,}$',
19
+            // ),
20
+        // ),
21
+    // TODO: Change if going international (only works in usa)
22
+    zipcode: Joi.string().min(5).max(5).pattern(new RegExp('^[0-9]{5}$')),
23
+    seeking: Joi.string(),
24
+    urgency: Joi.string(),
25
+    presence: Joi.string(),
26
+    duration: Joi.string(),
27
+    pronouns: Joi.string(),
28
+    language: Joi.string(),
29
+    image: Joi.any(),
30
+    // NOTE: Allows 1 to 3 digits and then distance metric
31
+    distance: Joi.string()
32
+        .min(4)
33
+        .max(15)
34
+        .pattern(new RegExp('^\\d{1,3}(\\.\\d{1,2})?\\s?(mi|km|mile|miles|kilometer|kilometers)$')),
35
+    blurb: Joi.string().max(200),
36
+    value: Joi.string(),
37
+    aspect: Joi.number(),
38
+}
39
+
40
+export { answerValidator }

+ 22
- 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,25 @@ 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
+        console.log(
41
+            'this.aspectQuestions: ',
42
+            JSON.stringify(this.aspectQuestions),
43
+        )
44
+    }
45
+
46
+    validateAnswer(payload) {
47
+        const { question, input } = payload
48
+
49
+        // Continue our ugly hacks
50
+        const validationType =
51
+            question.category == 'aspect'
52
+                ? question.category
53
+                : question.survey_stage
54
+        const validate = answerValidator[validationType].validate(input)
55
+        if (validate.error) {
56
+            console.error(`error: ${validate.error}`)
57
+        }
58
+        return !validate.error ? true : false
39 59
     }
40 60
 
41 61
     isValid() {

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


+ 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

+ 80
- 39
frontend/src/utils/lang.js Ver arquivo

@@ -1,27 +1,67 @@
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
+// Easily reorder steps of survey here:
4 19
 const allSteps = {
5 20
     usa: {
6
-        splash: 'splash',
7 21
         name: 'name',
8
-        email: 'email',
22
+        aspect01: 'aspect-1',
23
+        aspect02: 'aspect-2',
24
+        aspect03: 'aspect-3',
9 25
         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',
18 26
         zipcode: 'zipcode',
19
-        // distance: 'distance',
20
-        blurb: 'blurb',
27
+        seeking: 'seeking',
28
+        urgency: 'urgency',
29
+        aspect04: 'aspect-4',
30
+        aspect05: 'aspect-5',
31
+        aspect06: 'aspect-6',
32
+        email: 'email',
33
+        presence: 'presence',
34
+        duration: 'duration',
35
+        pronouns: 'pronouns',
36
+        language: 'language',
21 37
         image: 'image',
22
-        aspects: 'aspects'
38
+        distance: 'distance',
39
+        blurb: 'blurb',
40
+        // experience: 'experience',
41
+        // roles: 'role',
23 42
     },
24 43
 }
44
+const surveyStages = {
45
+    7: allSteps.usa.name,
46
+    8: allSteps.usa.email,
47
+    9: allSteps.usa.password,
48
+    10: allSteps.usa.zipcode,
49
+    11: allSteps.usa.seeking,
50
+    12: allSteps.usa.image,
51
+    13: allSteps.usa.language,
52
+    14: allSteps.usa.duration,
53
+    15: allSteps.usa.presence,
54
+    16: allSteps.usa.blurb,
55
+    17: allSteps.usa.urgency,
56
+    18: allSteps.usa.pronouns,
57
+    19: allSteps.usa.distance,
58
+    1: allSteps.usa.aspect01,
59
+    2: allSteps.usa.aspect02,
60
+    3: allSteps.usa.aspect03,
61
+    4: allSteps.usa.aspect04,
62
+    5: allSteps.usa.aspect05,
63
+    6: allSteps.usa.aspect06,
64
+}
25 65
 
26 66
 const aspectResponses = {
27 67
     usa: {
@@ -52,33 +92,34 @@ possible.usa = {
52 92
     // key 10
53 93
     duration: ['full-time', 'part-time', 'contract', 'flexible'],
54 94
     // 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
-    },
95
+    //     experience: ['associate', 'junior', 'mid-level', 'senior', 'staff'],
96
+    // roles: {
97
+    // type: [
98
+    // 'back-end',
99
+    // 'database',
100
+    // 'front-end',
101
+    // 'full-stack',
102
+    // 'qa',
103
+    // 'security',
104
+    // 'system',
105
+    // 'test',
106
+    // ],
107
+    // position: [
108
+    // 'administrator',
109
+    // 'analyst',
110
+    // 'architect',
111
+    // 'developer',
112
+    // 'engineer',
113
+    // 'manager',
114
+    // 'technician',
115
+    // ],
116
+    // candidate: ['hiring_manager', 'recruiter'],
117
+    // },
78 118
     pronouns: ['she/her', 'she/they', 'he/him', 'he/they', 'they/them'],
119
+    //    role: ['role1', 'role2'],
79 120
     image: [],
80 121
     zipcode: [],
81 122
     blurb: [],
82 123
 }
83 124
 
84
-export { allSteps, aspectResponses, possible, DELIMITER }
125
+export { allSteps, splash, surveyStages, aspectResponses, aspectsArr, possible }

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

@@ -1,76 +1,78 @@
1 1
 import { Survey } from '../entities/index.js'
2 2
 import { fetchQuestions } from '../services/index.js'
3
-import { possible } from './lang.js'
4
-
5
-const promptToComponent = {
6
-    splash: 'Splash',
7
-    name: 'FormInput',
8
-    email: 'FormInput',
9
-    password: 'FormInput',
10
-    zipcode: 'FormInput',
11
-    // seeking: 'FormDropdown',
12
-    // urgency: 'FormDropdown',
13
-    // presence: 'FormDropdown',
14
-    // duration: 'FormDropdown',
15
-    // experience: 'FormTags',
16
-    // pronouns: 'FormDropdown',
17
-    // language: 'FormDropdown',
18
-    image: 'FormInput',
19
-    // distance: 'FormInput',
20
-    blurb: 'FormInput',
21
-    aspects: 'Aspects'
22
-}
23
-/**
24
- * Make a step from match or step information
25
- * @param {object} match
26
- * @param {object} step
27
- * @returns something like a response_key with possible responses
28
- */
29
-const formatStep = (match, step) => {
30
-    const responsesByCategory = possible['usa']
31
-    const responseKey = {
32
-        response_key_id: match ? match.response_key_id : null,
33
-        response_key_category: match ? match.response_key_category : 'profile',
34
-        response_key_prompt: match ? match.response_key_prompt : step,
35
-        response_key_description: match ? match.response_key_description : null,
36
-    }
37
-    return {
38
-        ...responseKey,
39
-        responses: responsesByCategory[step] ? responsesByCategory[step] : [],
40
-    }
41
-}
42
-const associateWithComponent = responseKeyLike => {
43
-    let component = promptToComponent[responseKeyLike.response_key_prompt]
44
-    return { ...responseKeyLike, component }
45
-}
46
-
47
-const hasMatch = (step, inArray) => {
48
-    return inArray.find(q => q.response_key_prompt == step)
49
-}
3
+import { splash, possible, surveyStages, allSteps } from './lang.js'
50 4
 
51 5
 class SurveyFactory {
52 6
     constructor() {
53 7
         this.questionsFromDb = []
54 8
     }
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)
9
+    _addResponses(responseKeys, responsesByCategory) {
10
+        const existingResponses = {}
11
+        // Removes empty form drop down options from possible['usa']
12
+        Object.keys(responsesByCategory).forEach(categoryKey => {
13
+            if (responsesByCategory[categoryKey].length) {
14
+                existingResponses[categoryKey] = responsesByCategory[categoryKey]
63 15
             }
64
-            const responseKeyLike = formatStep(match, step)
65
-            const withComponent = associateWithComponent(responseKeyLike)
66
-            console.log('withComponent :>> ', withComponent)
67
-            return withComponent
68 16
         })
69
-        // temporary extra condition in filter 
70
-        let unseen = this.questionsFromDb.filter(
71
-            q => !seenIds.includes(q.response_key_id) && [1,2,3,4,5,6].includes(q.response_key_id),
72
-        )
73
-        return [...stepsInCommon, ...unseen]
17
+        // Adds form drop down options to each responseKey
18
+        Object.keys(existingResponses).forEach(inputKey => {
19
+            responseKeys.forEach(responseKey => {
20
+                if (responseKey.survey_stage == inputKey) {
21
+                    responseKey.responses = existingResponses[inputKey]
22
+                }
23
+            })
24
+        })
25
+        return responseKeys
26
+    }
27
+    _addComponents(responseKeys) {
28
+        responseKeys.forEach(responseKey => {
29
+            switch (responseKey.category) {
30
+                case 'input':
31
+                    responseKey.component = 'FormInput'
32
+                    break
33
+                case 'choice':
34
+                    responseKey.component = 'FormDropdown'
35
+                    break
36
+                case 'aspect':
37
+                    responseKey.component = 'QuestionResponse'
38
+                    break
39
+            }
40
+        })
41
+        return responseKeys
42
+    }
43
+    _addSurveySteps(responseKeys, surveyStages) {
44
+        responseKeys.forEach(responseKey => {
45
+            Object.keys(surveyStages).forEach((stage, i) => {
46
+                if (responseKey.response_key_id == stage) {
47
+                    responseKey.survey_stage = surveyStages[i + 1]
48
+                }
49
+            })
50
+        })
51
+        return responseKeys
52
+    }
53
+    // TODO: Don't nest the for loop...
54
+    _sortSurveySteps(mutatedResponseKeys, allSteps) {
55
+        const reordered = []
56
+        Object.values(allSteps).forEach(step => {
57
+            Object.values(mutatedResponseKeys).forEach(response => {
58
+                if (surveyStages[response.response_key_id] === step)  {
59
+                    response.survey_stage = step
60
+                    reordered.push(response)
61
+                }
62
+            })
63
+        })
64
+        return reordered
65
+    }
66
+    _setSteps() {
67
+        const responseKeys = this.questionsFromDb
68
+        const responsesByCategory = possible['usa']
69
+        let mutatedResponseKeys = this._addSurveySteps(responseKeys, surveyStages)
70
+        mutatedResponseKeys = this._addResponses(mutatedResponseKeys, responsesByCategory)
71
+        mutatedResponseKeys = this._addComponents(responseKeys)
72
+        mutatedResponseKeys = this._sortSurveySteps(mutatedResponseKeys, allSteps['usa'])
73
+        // Splash page is placed at beginning of survey
74
+        mutatedResponseKeys.unshift(splash)
75
+        return mutatedResponseKeys
74 76
     }
75 77
     async getQuestions() {
76 78
         try {
@@ -80,14 +82,14 @@ class SurveyFactory {
80 82
             console.error(err)
81 83
         }
82 84
     }
83
-    async createSurvey(langFile, roleTree) {
85
+    async createSurvey(roleTree) {
84 86
         if (!this.questionsFromDb.length) {
85 87
             const res = await this.getQuestions()
86 88
             console.warn(
87 89
                 `Attempted to create a survey before getting questions: retrieved ${res.length} questions`,
88 90
             )
89 91
         }
90
-        const steps = this._setSteps(langFile)
92
+        const steps = this._setSteps()
91 93
         return new Survey(steps, roleTree)
92 94
     }
93 95
 }

+ 43
- 8
frontend/src/views/OnboardingView.vue Ver arquivo

@@ -2,23 +2,41 @@
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
+        .answers(v-for='(value, key) in answered')
8
+            span(v-if='key == "name" && value && currentStep == 2') Hi {{ value }}!
9
+            span(v-if='key == "email" && value && currentStep == 3') Thanks for the contact info, {{ answered.name }}!
10
+
11
+        //- h3(v-if='currentStep == 1') Welcome to Siimee Onboarding! Let's get started!
12
+        br
7 13
         .step(v-for='(step, i) in survey.steps')
8 14
             component(
9
-                :aspect-questions='step.component == "Aspects" ? survey.aspectQuestions : null'
10 15
                 :is='step.component'
11 16
                 :question='step'
17
+                :currentStep='currentStep'
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>
19 37
 import { surveyFactory } from '@/utils'
20
-import { allSteps } from '@/utils/lang'
21 38
 import stepViews from '@/components/onboarding'
39
+import SurveyCompleteView from './SurveyCompleteView.vue'
22 40
 
23 41
 // import savesurveybyprfileid - call it on submit
24 42
 // paginate to save every steps answers
@@ -26,15 +44,17 @@ export default {
26 44
     name: 'OnboardingView',
27 45
     components: {
28 46
         ...stepViews,
47
+        SurveyCompleteView,
29 48
     },
30 49
     data: () => ({
31 50
         answered: {},
32 51
         aspectQuestions: [],
33 52
         currentStep: 0,
34 53
         survey: null,
54
+        invalidResponse: false,
35 55
     }),
36 56
     async created() {
37
-        this.survey = await surveyFactory.createSurvey(allSteps['usa'])
57
+        this.survey = await surveyFactory.createSurvey()
38 58
     },
39 59
     methods: {
40 60
         onSubmit() {
@@ -46,13 +66,25 @@ export default {
46 66
         updateAnswers(payload) {
47 67
             // null payload is passed on splash page
48 68
             if (payload) {
49
-                const k = payload.question.response_key_prompt
50
-                this.answered[k] = payload.answer
51
-                console.log(`${k}:`, this.answered[k])
69
+                this.invalidResponse = false
70
+                const k = payload.question.survey_stage
71
+                this.answered[k] = payload.input
72
+
73
+                if (!this.survey.validateAnswer(payload)) {
74
+                    this.invalidResponse = true
75
+                    return
76
+                }
77
+
78
+                // once validated, don't log password in answered object
79
+                this.answered[k] = k === 'password' ? undefined : payload.input
52 80
                 console.log(`Updated answers: ${JSON.stringify(this.answered)}`)
53 81
                 if (k === 'aspects') return
54 82
             }
55
-            this.goToStep(this.currentStep + 1)
83
+            if (this.currentStep > this.survey.steps.length) {
84
+                this.onSubmit(this.answered)
85
+            } else {
86
+                this.goToStep(this.currentStep + 1)
87
+            }
56 88
         },
57 89
     },
58 90
 }
@@ -70,6 +102,9 @@ export default {
70 102
     article
71 103
         height: 100vh
72 104
 
105
+    .answers
106
+        text-align: center
107
+
73 108
     .w-button
74 109
             display: flex
75 110
             width: 315px

+ 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>

Carregando…
Cancelar
Salvar