Passed
Push — dev ( 215397...f312c9 )
by Tristan
06:19 queued 16s
created

resources/assets/js/components/Application/ExperienceAccordions/ExperienceAccordionCommon.tsx   A

Complexity

Total Complexity 5
Complexity/F 0

Size

Lines of Code 448
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 5
eloc 340
mnd 5
bc 5
fnc 0
dl 0
loc 448
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { ReactElement, ReactNode, useState } from "react";
2
import { FormattedMessage, useIntl, IntlShape } from "react-intl";
3
import { accordionMessages } from "../applicationMessages";
4
import {
5
  Locales,
6
  getLocale,
7
  localizeFieldNonNull,
8
} from "../../../helpers/localize";
9
import { readableDate } from "../../../helpers/dates";
10
import { ExperienceSkill, Skill } from "../../../models/types";
11
import { getId, hasKey, mapToObject } from "../../../helpers/queries";
12
13
export const titleBarDateRange = (
14
  startDate: Date,
15
  endDate: Date | null,
16
  isActive: boolean,
17
  intl: IntlShape,
18
  locale: Locales,
19
): string => {
20
  if (isActive || endDate === null) {
21
    return intl.formatMessage(accordionMessages.dateRangeCurrent, {
22
      startDate: readableDate(locale, startDate),
23
    });
24
  }
25
  return intl.formatMessage(accordionMessages.dateRange, {
26
    startDate: readableDate(locale, startDate),
27
    endDate: readableDate(locale, endDate),
28
  });
29
};
30
31
interface ExperienceAccordionSkillsProps {
32
  relevantSkills: ExperienceSkill[];
33
  irrelevantSkillCount: number;
34
  skillsById: { [id: number]: Skill };
35
  showSkillDetails: boolean;
36
  handleEditSkill?: (experienceSkillId: number) => void;
37
}
38
39
export const ExperienceAccordionSkills: React.FC<ExperienceAccordionSkillsProps> = ({
40
  relevantSkills,
41
  irrelevantSkillCount,
42
  skillsById,
43
  showSkillDetails,
44
  handleEditSkill,
45
}) => {
46
  const intl = useIntl();
47
  const locale = getLocale(intl.locale);
48
  const relevantSkillCount = relevantSkills.length;
49
50
  const renderDetailedSkill = (
51
    experienceSkill: ExperienceSkill,
52
  ): React.ReactElement | null => {
53
    const skill = hasKey(skillsById, experienceSkill.skill_id)
54
      ? skillsById[experienceSkill.skill_id]
55
      : null;
56
    if (skill === null) {
57
      return null;
58
    }
59
    return (
60
      <div key={skill.id} data-c-grid-item="base(1of1)">
61
        <p>
62
          <span data-c-tag="c1" data-c-radius="pill" data-c-font-size="small">
63
            {localizeFieldNonNull(locale, skill, "name")}
64
          </span>
65
        </p>
66
        <p data-c-font-style="italic" data-c-margin="top(.5)">
67
          {experienceSkill.justification}
68
        </p>
69
        {handleEditSkill && (
70
          <div data-c-margin="top(1)" data-c-alignment="base(centre) pl(right)">
71
            <button
72
              data-c-button="solid(c1)"
73
              data-c-radius="rounded"
74
              type="button"
75
              onClick={(): void => handleEditSkill(experienceSkill.id)}
76
            >
77
              <FormattedMessage
78
                id="application.experienceAccordion.editExperienceSkill"
79
                defaultMessage="Edit Skill"
80
              />
81
            </button>
82
          </div>
83
        )}
84
      </div>
85
    );
86
  };
87
  const renderSimpleSkill = (
88
    experienceSkill: ExperienceSkill,
89
  ): React.ReactElement | null => {
90
    const skill = hasKey(skillsById, experienceSkill.skill_id)
91
      ? skillsById[experienceSkill.skill_id]
92
      : null;
93
    if (skill === null) {
94
      return null;
95
    }
96
    return (
97
      <span
98
        key={skill.id}
99
        data-c-tag="c1"
100
        data-c-radius="pill"
101
        data-c-font-size="small"
102
        data-c-margin="lr(.5)"
103
      >
104
        {localizeFieldNonNull(locale, skill, "name")}
105
      </span>
106
    );
107
  };
108
109
  return (
110
    <div data-c-grid-item="base(1of1)" data-c-margin="top(1)">
111
      <h4
112
        data-c-color="c2"
113
        data-c-font-weight="bold"
114
        data-c-margin="bottom(.5)"
115
      >
116
        <FormattedMessage
117
          id="application.experienceAccordion.skillsTitle"
118
          defaultMessage="Skills for this Job"
119
          description="Subtitle of the skills section."
120
        />
121
      </h4>
122
      <div data-c-grid="gutter(all, 1)">
123
        {showSkillDetails && relevantSkills.map(renderDetailedSkill)}
124
        {!showSkillDetails && (
125
          <div data-c-grid-item="base(1of1)">
126
            {relevantSkills.map(renderSimpleSkill)}
127
          </div>
128
        )}
129
        {irrelevantSkillCount > 0 && (
130
          <div data-c-grid-item="base(1of1)">
131
            <p
132
              data-c-font-size="small"
133
              data-c-color="gray"
134
              data-c-margin="bottom(1)"
135
            >
136
              <FormattedMessage
137
                id="application.experienceAccordion.irrelevantSkillCount"
138
                defaultMessage="There {skillCount, plural, one {is <b>#</b> other unrelated skill} other {are <b>#</b> other unrelated skills}} attached to this experience. You can see {skillCount, plural, one {it} other {them}} on your profile."
139
                description="Say how many skills unrelated to this job are associated with this experience."
140
                values={{
141
                  skillCount: irrelevantSkillCount,
142
                  b: (...chunks) => (
143
                    <span data-c-font-weight="bold">{chunks}</span>
144
                  ),
145
                }}
146
              />
147
            </p>
148
          </div>
149
        )}
150
        {irrelevantSkillCount === 0 && relevantSkillCount === 0 && (
151
          <div data-c-grid-item="base(1of1)">
152
            <p data-c-color="gray" data-c-margin="bottom(1)">
153
              <FormattedMessage
154
                id="application.experienceAccordion.noSkills"
155
                defaultMessage="You don't have any skills attached to this experience."
156
                description="Message to show if experience has no associated skills at all."
157
              />
158
            </p>
159
          </div>
160
        )}
161
      </div>
162
    </div>
163
  );
164
};
165
166
export const ExperienceAccordionEducation: React.FC = () => {
167
  return (
168
    <div data-c-grid-item="base(1of1)" data-c-margin="top(1)">
169
      <h4
170
        data-c-color="c2"
171
        data-c-font-weight="bold"
172
        data-c-margin="bottom(.5)"
173
      >
174
        <i
175
          className="fas fa-check-circle"
176
          data-c-margin="right(.25)"
177
          data-c-color="go"
178
        />
179
        <FormattedMessage
180
          id="application.experienceAccordion.educationRequirement"
181
          defaultMessage="Education Requirement"
182
        />
183
      </h4>
184
      <p data-c-margin="bottom(1)">
185
        <FormattedMessage
186
          id="application.experienceAccordion.educationRequirmentDescription"
187
          defaultMessage="You've selected this experience as an indicator of how you meet the education requirements for this job."
188
          description="Explanation of what it means that this experience meets an education requirement."
189
        />
190
      </p>
191
    </div>
192
  );
193
};
194
195
interface ExperienceAccordionButtonsProps {
196
  handleEdit: () => void;
197
  handleDelete: () => Promise<void>;
198
}
199
200
export const ExperienceAccordionButtons: React.FC<ExperienceAccordionButtonsProps> = ({
201
  handleEdit,
202
  handleDelete,
203
}) => {
204
  const [disableButtons, setDisableButtons] = useState(false);
205
  return (
206
    <div data-c-padding="top(1) lr(2)">
207
      <div data-c-grid="gutter(all, 1) middle">
208
        <div data-c-grid-item="tp(1of2)" data-c-align="base(center) tp(left)">
209
          <button
210
            data-c-button="outline(c1)"
211
            data-c-radius="rounded"
212
            type="button"
213
            disabled={disableButtons}
214
            onClick={(): void => {
215
              setDisableButtons(true);
216
              handleDelete().finally(() => {
217
                setDisableButtons(false);
218
              });
219
            }}
220
          >
221
            <FormattedMessage
222
              id="application.experienceAccordion.deleteButton"
223
              defaultMessage="Delete Experience"
224
            />
225
          </button>
226
        </div>
227
        <div data-c-grid-item="tp(1of2)" data-c-align="base(center) tp(right)">
228
          <button
229
            data-c-button="solid(c1)"
230
            data-c-radius="rounded"
231
            type="button"
232
            disabled={disableButtons}
233
            onClick={handleEdit}
234
          >
235
            <FormattedMessage
236
              id="application.experienceAccordion.editButton"
237
              defaultMessage="Edit Experience"
238
            />
239
          </button>
240
        </div>
241
      </div>
242
    </div>
243
  );
244
};
245
246
interface AccordionWrapperProps {
247
  title: ReactNode | string;
248
  subtitle: string;
249
  relatedSkillCount: number;
250
  isEducationJustification: boolean;
251
  iconClass: string;
252
}
253
254
export const ExperienceAccordionWrapper: React.FC<AccordionWrapperProps> = ({
255
  title,
256
  subtitle,
257
  relatedSkillCount,
258
  isEducationJustification,
259
  iconClass,
260
  children,
261
}) => {
262
  const intl = useIntl();
263
  const [isExpanded, setIsExpanded] = useState(false);
264
  return (
265
    <div
266
      data-c-accordion
267
      data-c-background="white(100)"
268
      data-c-card=""
269
      data-c-margin="bottom(.5)"
270
      className={`${isExpanded && "active"}`}
271
    >
272
      <button
273
        tabIndex={0}
274
        aria-expanded={isExpanded}
275
        data-c-accordion-trigger
276
        type="button"
277
        onClick={(): void => {
278
          setIsExpanded(!isExpanded);
279
        }}
280
      >
281
        <div data-c-grid="">
282
          <div data-c-grid-item="base(1of4) tl(1of6) equal-col">
283
            <div className="experience-type-indicator">
284
              <i
285
                className={`fas ${iconClass}`}
286
                data-c-color="c1"
287
                data-c-font-size="h4"
288
              />
289
            </div>
290
          </div>
291
          <div data-c-grid-item="base(3of4) tl(5of6)">
292
            <div data-c-padding="all(1)">
293
              <div data-c-grid="middle">
294
                <div data-c-grid-item="tl(3of4)">
295
                  <p>{title}</p>
296
                  <p
297
                    data-c-margin="top(quarter)"
298
                    data-c-colour="c1"
299
                    data-c-font-size="small"
300
                  >
301
                    {subtitle}
302
                  </p>
303
                </div>
304
                <div data-c-grid-item="tl(1of4)" data-c-align="base(left)">
305
                  <FormattedMessage
306
                    id="application.experienceAccordion.skillCount"
307
                    defaultMessage="{skillCount, plural, =0 {No related skills} one {# related skill} other {# related skills}} {isEducationJustification, select, true {/ Education Requirement} false {}}"
308
                    description="Displays the number of required skills this relates to, and whether it's used to meed education requirements."
309
                    values={{
310
                      skillCount: relatedSkillCount,
311
                      isEducationJustification,
312
                    }}
313
                  />
314
                </div>
315
              </div>
316
            </div>
317
          </div>
318
        </div>
319
        <span data-c-visibility="invisible">
320
          {intl.formatMessage(accordionMessages.expand)}
321
        </span>
322
        <i
323
          aria-hidden="true"
324
          className="fas fa-angle-down"
325
          data-c-accordion-add=""
326
          data-c-colour="black"
327
        />
328
        <i
329
          aria-hidden="true"
330
          className="fas fa-angle-up"
331
          data-c-accordion-remove=""
332
          data-c-colour="black"
333
        />
334
      </button>
335
      <div
336
        aria-hidden="true"
337
        data-c-accordion-content=""
338
        data-c-background="gray(10)"
339
        data-c-padding="bottom(2)"
340
      >
341
        <hr data-c-hr="thin(gray)" data-c-margin="bottom(2)" />
342
        <div data-c-padding="lr(2)">{children}</div>
343
      </div>
344
    </div>
345
  );
346
};
347
348
interface ApplicationExperienceAccordionProps {
349
  title: ReactNode | string;
350
  subtitle: string;
351
  iconClass: string;
352
  relevantSkills: ExperienceSkill[];
353
  skills: Skill[];
354
  irrelevantSkillCount: number;
355
  isEducationJustification: boolean;
356
  showSkillDetails: boolean;
357
  showButtons: boolean;
358
  handleDelete: () => Promise<void>;
359
  handleEdit: () => void;
360
}
361
362
export const ApplicationExperienceAccordion: React.FC<ApplicationExperienceAccordionProps> = ({
363
  title,
364
  subtitle,
365
  iconClass,
366
  relevantSkills,
367
  skills,
368
  irrelevantSkillCount,
369
  isEducationJustification,
370
  showSkillDetails,
371
  showButtons,
372
  handleDelete,
373
  handleEdit,
374
  children,
375
}) => {
376
  return (
377
    <ExperienceAccordionWrapper
378
      title={title}
379
      subtitle={subtitle}
380
      relatedSkillCount={relevantSkills.length}
381
      isEducationJustification={isEducationJustification}
382
      iconClass={iconClass}
383
    >
384
      {children}
385
      <ExperienceAccordionSkills
386
        relevantSkills={relevantSkills}
387
        irrelevantSkillCount={irrelevantSkillCount}
388
        skillsById={mapToObject(skills, getId)}
389
        showSkillDetails={showSkillDetails}
390
      />
391
      {isEducationJustification && <ExperienceAccordionEducation />}
392
      {showButtons && (
393
        <ExperienceAccordionButtons
394
          handleEdit={handleEdit}
395
          handleDelete={handleDelete}
396
        />
397
      )}
398
    </ExperienceAccordionWrapper>
399
  );
400
};
401
402
interface ProfileExperienceAccordionProps {
403
  title: ReactNode | string;
404
  subtitle: string;
405
  iconClass: string;
406
  relevantSkills: ExperienceSkill[];
407
  skillsById: { [id: number]: Skill };
408
  handleDelete: () => Promise<void>;
409
  handleEdit: () => void;
410
  handleEditSkill: (experienceSkillId: number) => void;
411
}
412
413
export const ProfileExperienceAccordion: React.FunctionComponent<ProfileExperienceAccordionProps> = ({
414
  title,
415
  subtitle,
416
  iconClass,
417
  relevantSkills,
418
  skillsById,
419
  handleDelete,
420
  handleEdit,
421
  handleEditSkill,
422
  children,
423
}) => {
424
  return (
425
    <ExperienceAccordionWrapper
426
      title={title}
427
      subtitle={subtitle}
428
      relatedSkillCount={relevantSkills.length}
429
      isEducationJustification={false}
430
      iconClass={iconClass}
431
    >
432
      {children}
433
      <ExperienceAccordionButtons
434
        handleEdit={handleEdit}
435
        handleDelete={handleDelete}
436
      />
437
      <hr data-c-hr="thin(gray)" data-c-margin="bottom(1) top(1)" />
438
      <ExperienceAccordionSkills
439
        relevantSkills={relevantSkills}
440
        irrelevantSkillCount={0}
441
        skillsById={skillsById}
442
        showSkillDetails
443
        handleEditSkill={handleEditSkill}
444
      />
445
    </ExperienceAccordionWrapper>
446
  );
447
};
448