1
|
|
|
/* eslint camelcase: "off", @typescript-eslint/camelcase: "off" */ |
2
|
|
|
import React, { useState } from "react"; |
3
|
|
|
import { |
4
|
|
|
FormattedMessage, |
5
|
|
|
useIntl, |
6
|
|
|
defineMessages, |
7
|
|
|
IntlShape, |
8
|
|
|
} from "react-intl"; |
9
|
|
|
import { Formik, Form, FastField } from "formik"; |
10
|
|
|
import * as Yup from "yup"; |
11
|
|
|
import { ExperienceSkill, Skill, Criteria } from "../../../models/types"; |
12
|
|
|
import { slugify } from "../../../helpers/routes"; |
13
|
|
|
import { getLocale, localizeFieldNonNull } from "../../../helpers/localize"; |
14
|
|
|
import { validationMessages } from "../../Form/Messages"; |
15
|
|
|
import { |
16
|
|
|
getExperienceHeading, |
17
|
|
|
getExperienceSubheading, |
18
|
|
|
getExperienceJustificationLabel, |
19
|
|
|
} from "../../../models/localizedConstants"; |
20
|
|
|
import { getId, hasKey, mapToObject } from "../../../helpers/queries"; |
21
|
|
|
import { getSkillLevelName } from "../../../models/jobUtil"; |
22
|
|
|
import AlertWhenUnsaved from "../../Form/AlertWhenUnsaved"; |
23
|
|
|
import TextAreaInput from "../../Form/TextAreaInput"; |
24
|
|
|
import WordCounter from "../../WordCounter/WordCounter"; |
25
|
|
|
import { countNumberOfWords } from "../../WordCounter/helpers"; |
26
|
|
|
|
27
|
|
|
const displayMessages = defineMessages({ |
28
|
|
|
sidebarLinkTitle: { |
29
|
|
|
id: "application.skills.sidebarLinkTitle", |
30
|
|
|
defaultMessage: "Go to this skill.", |
31
|
|
|
description: "Title attribute for sidebar links.", |
32
|
|
|
}, |
33
|
|
|
accessibleAccordionButtonText: { |
34
|
|
|
id: "application.skills.accessibleAccordionButtonText", |
35
|
|
|
defaultMessage: "Click to view...", |
36
|
|
|
description: "Hidden accordion button text for accessibility.", |
37
|
|
|
}, |
38
|
|
|
save: { |
39
|
|
|
id: "application.skills.saveButtonText", |
40
|
|
|
defaultMessage: "Save", |
41
|
|
|
description: "Button text for saving an experience skill justification.", |
42
|
|
|
}, |
43
|
|
|
saved: { |
44
|
|
|
id: "application.skills.savedButtonText", |
45
|
|
|
defaultMessage: "Saved", |
46
|
|
|
description: |
47
|
|
|
"Button text for after an experience skill justification is saved.", |
48
|
|
|
}, |
49
|
|
|
wordCountUnderMax: { |
50
|
|
|
id: "application.skills.wordCountUnderMax", |
51
|
|
|
defaultMessage: " words left.", |
52
|
|
|
description: |
53
|
|
|
"Message displayed next to word counter when user is under the maximum count.", |
54
|
|
|
}, |
55
|
|
|
wordCountOverMax: { |
56
|
|
|
id: "application.skills.wordCountOverMax", |
57
|
|
|
defaultMessage: " words over the limit.", |
58
|
|
|
description: |
59
|
|
|
"Message displayed next to word counter when user is over the maximum count.", |
60
|
|
|
}, |
61
|
|
|
}); |
62
|
|
|
|
63
|
|
|
const JUSTIFICATION_WORD_LIMIT = 100; |
64
|
|
|
|
65
|
|
|
enum IconStatus { |
66
|
|
|
COMPLETE = "fas fa-check-circle", |
67
|
|
|
DEFAULT = "far fa-circle", |
68
|
|
|
ERROR = "fas fa-exclamation-circle", |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
interface StatusIconProps { |
72
|
|
|
status: IconStatus; |
73
|
|
|
size: string; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
const StatusIcon: React.FC<StatusIconProps> = ({ |
77
|
|
|
status, |
78
|
|
|
size, |
79
|
|
|
}): React.ReactElement => { |
80
|
|
|
let color: string; |
81
|
|
|
switch (status) { |
82
|
|
|
case IconStatus.COMPLETE: |
83
|
|
|
color = "go"; |
84
|
|
|
break; |
85
|
|
|
case IconStatus.ERROR: |
86
|
|
|
color = "stop"; |
87
|
|
|
break; |
88
|
|
|
default: |
89
|
|
|
color = "c1"; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
return <i className={status} data-c-color={color} data-c-font-size={size} />; |
93
|
|
|
}; |
94
|
|
|
|
95
|
|
|
interface SidebarProps { |
96
|
|
|
menuSkills: string[]; |
97
|
|
|
intl: IntlShape; |
98
|
|
|
status: { |
99
|
|
|
[k: string]: IconStatus; |
100
|
|
|
}; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
const Sidebar: React.FC<SidebarProps> = ({ menuSkills, intl, status }) => { |
104
|
|
|
return ( |
105
|
|
|
<div data-c-padding="top(3)" className="application-skill-navigation"> |
106
|
|
|
<p |
107
|
|
|
data-c-font-size="h3" |
108
|
|
|
data-c-font-weight="bold" |
109
|
|
|
data-c-margin="bottom(1)" |
110
|
|
|
> |
111
|
|
|
<FormattedMessage |
112
|
|
|
id="application.skills.sidebarHeading" |
113
|
|
|
defaultMessage="On this page:" |
114
|
|
|
description="Heading for the sidebar on the Skills page." |
115
|
|
|
/> |
116
|
|
|
</p> |
117
|
|
|
<ul> |
118
|
|
|
{menuSkills.map((skillName: string) => ( |
119
|
|
|
<li key={slugify(skillName)}> |
120
|
|
|
<StatusIcon status={status[slugify(skillName)]} size="" /> |
121
|
|
|
<a |
122
|
|
|
href={`#${slugify(skillName)}`} |
123
|
|
|
title={intl.formatMessage(displayMessages.sidebarLinkTitle)} |
124
|
|
|
> |
125
|
|
|
{skillName} |
126
|
|
|
</a> |
127
|
|
|
</li> |
128
|
|
|
))} |
129
|
|
|
</ul> |
130
|
|
|
</div> |
131
|
|
|
); |
132
|
|
|
}; |
133
|
|
|
|
134
|
|
|
interface ExperienceAccordionProps { |
135
|
|
|
experienceSkill: ExperienceSkill; |
136
|
|
|
intl: IntlShape; |
137
|
|
|
status: { |
138
|
|
|
[k: string]: IconStatus; |
139
|
|
|
}; |
140
|
|
|
skillName: string; |
141
|
|
|
handleUpdateExperienceJustification: ( |
142
|
|
|
experience: ExperienceSkill, |
143
|
|
|
) => Promise<ExperienceSkill>; |
144
|
|
|
handleUpdateStatus: React.Dispatch< |
145
|
|
|
React.SetStateAction<{ |
146
|
|
|
[k: string]: IconStatus; |
147
|
|
|
}> |
148
|
|
|
>; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
interface ExperienceSkillFormValues { |
152
|
|
|
justification: string; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
const ExperienceAccordion: React.FC<ExperienceAccordionProps> = ({ |
156
|
|
|
experienceSkill, |
157
|
|
|
intl, |
158
|
|
|
status, |
159
|
|
|
skillName, |
160
|
|
|
handleUpdateExperienceJustification, |
161
|
|
|
handleUpdateStatus, |
162
|
|
|
}) => { |
163
|
|
|
const [isExpanded, setIsExpanded] = useState(false); |
164
|
|
|
let heading = ""; |
165
|
|
|
let subHeading = ""; |
166
|
|
|
let label = ""; |
167
|
|
|
|
168
|
|
|
const initialValues: ExperienceSkillFormValues = { |
169
|
|
|
justification: experienceSkill.justification || "", |
170
|
|
|
}; |
171
|
|
|
|
172
|
|
|
const experienceSkillSchema = Yup.object().shape({ |
173
|
|
|
justification: Yup.string() |
174
|
|
|
.test( |
175
|
|
|
"wordCount", |
176
|
|
|
intl.formatMessage(validationMessages.overMaxWords, { |
177
|
|
|
numberOfWords: JUSTIFICATION_WORD_LIMIT, |
178
|
|
|
}), |
179
|
|
|
(value) => countNumberOfWords(value) <= JUSTIFICATION_WORD_LIMIT, |
180
|
|
|
) |
181
|
|
|
.required(intl.formatMessage(validationMessages.required)), |
182
|
|
|
}); |
183
|
|
|
|
184
|
|
|
if (experienceSkill.experience !== null) { |
185
|
|
|
heading = getExperienceHeading(experienceSkill.experience, intl); |
186
|
|
|
subHeading = getExperienceSubheading(experienceSkill.experience, intl); |
187
|
|
|
label = getExperienceJustificationLabel( |
188
|
|
|
experienceSkill.experience, |
189
|
|
|
intl, |
190
|
|
|
skillName, |
191
|
|
|
); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
const handleExpandClick = (): void => { |
195
|
|
|
setIsExpanded(!isExpanded); |
196
|
|
|
}; |
197
|
|
|
|
198
|
|
|
const updateExperienceSkill = ( |
199
|
|
|
oldExperience: ExperienceSkill, |
200
|
|
|
values: ExperienceSkillFormValues, |
201
|
|
|
): ExperienceSkill => { |
202
|
|
|
const experienceJustification: ExperienceSkill = { |
203
|
|
|
...oldExperience, |
204
|
|
|
justification: values.justification || "", |
205
|
|
|
}; |
206
|
|
|
return experienceJustification; |
207
|
|
|
}; |
208
|
|
|
|
209
|
|
|
return ( |
210
|
|
|
<Formik |
211
|
|
|
initialValues={initialValues} |
212
|
|
|
validationSchema={experienceSkillSchema} |
213
|
|
|
onSubmit={(values, { setSubmitting, resetForm }): void => { |
214
|
|
|
const experienceJustification = updateExperienceSkill( |
215
|
|
|
experienceSkill, |
216
|
|
|
values, |
217
|
|
|
); |
218
|
|
|
handleUpdateExperienceJustification(experienceJustification) |
219
|
|
|
.then(() => { |
220
|
|
|
handleUpdateStatus({ |
221
|
|
|
...status, |
222
|
|
|
[slugify(skillName)]: IconStatus.COMPLETE, |
223
|
|
|
}); |
224
|
|
|
setSubmitting(false); |
225
|
|
|
resetForm(); |
226
|
|
|
}) |
227
|
|
|
.catch(() => { |
228
|
|
|
setSubmitting(false); |
229
|
|
|
}); |
230
|
|
|
}} |
231
|
|
|
> |
232
|
|
|
{({ dirty, isSubmitting }): React.ReactElement => ( |
233
|
|
|
<div |
234
|
|
|
data-c-accordion="" |
235
|
|
|
data-c-background="white(100)" |
236
|
|
|
data-c-card="" |
237
|
|
|
data-c-margin="bottom(.5)" |
238
|
|
|
className={`application-skill-explanation${ |
239
|
|
|
isExpanded ? " active" : "" |
240
|
|
|
}`} |
241
|
|
|
> |
242
|
|
|
<button |
243
|
|
|
aria-expanded={isExpanded ? "true" : "false"} |
244
|
|
|
data-c-accordion-trigger="" |
245
|
|
|
tabIndex={0} |
246
|
|
|
type="button" |
247
|
|
|
onClick={handleExpandClick} |
248
|
|
|
> |
249
|
|
|
<div data-c-grid=""> |
250
|
|
|
<div data-c-grid-item="base(1of4) tl(1of6) equal-col"> |
251
|
|
|
<div className="skill-status-indicator"> |
252
|
|
|
<StatusIcon status={status[slugify(skillName)]} size="h4" /> |
253
|
|
|
</div> |
254
|
|
|
</div> |
255
|
|
|
<div data-c-grid-item="base(3of4) tl(5of6)"> |
256
|
|
|
<div data-c-padding="all(1)"> |
257
|
|
|
<div data-c-grid="middle"> |
258
|
|
|
<div data-c-grid-item="tl(3of4)"> |
259
|
|
|
<p>{heading}</p> |
260
|
|
|
<p |
261
|
|
|
data-c-margin="top(quarter)" |
262
|
|
|
data-c-colour="c1" |
263
|
|
|
data-c-font-size="small" |
264
|
|
|
> |
265
|
|
|
{subHeading} |
266
|
|
|
</p> |
267
|
|
|
</div> |
268
|
|
|
<div |
269
|
|
|
data-c-grid-item="tl(1of4)" |
270
|
|
|
data-c-align="base(left) tl(center)" |
271
|
|
|
> |
272
|
|
|
{experienceSkill.justification.length === 0 && ( |
273
|
|
|
<span data-c-color="stop" className="missing-info"> |
274
|
|
|
<FormattedMessage |
275
|
|
|
id="application.skills.justificationMissing" |
276
|
|
|
defaultMessage="Missing Information" |
277
|
|
|
description="Accordion heading error that displays when the justification is empty." |
278
|
|
|
/> |
279
|
|
|
</span> |
280
|
|
|
)} |
281
|
|
|
</div> |
282
|
|
|
</div> |
283
|
|
|
</div> |
284
|
|
|
</div> |
285
|
|
|
</div> |
286
|
|
|
<span data-c-visibility="invisible"> |
287
|
|
|
{intl.formatMessage( |
288
|
|
|
displayMessages.accessibleAccordionButtonText, |
289
|
|
|
)} |
290
|
|
|
</span> |
291
|
|
|
{isExpanded ? ( |
292
|
|
|
<i |
293
|
|
|
aria-hidden="true" |
294
|
|
|
className="fas fa-angle-up" |
295
|
|
|
data-c-colour="black" |
296
|
|
|
data-c-accordion-remove="" |
297
|
|
|
/> |
298
|
|
|
) : ( |
299
|
|
|
<i |
300
|
|
|
aria-hidden="true" |
301
|
|
|
className="fas fa-angle-down" |
302
|
|
|
data-c-colour="black" |
303
|
|
|
data-c-accordion-add="" |
304
|
|
|
/> |
305
|
|
|
)} |
306
|
|
|
</button> |
307
|
|
|
{isExpanded && ( |
308
|
|
|
<div |
309
|
|
|
aria-hidden={isExpanded ? "false" : "true"} |
310
|
|
|
data-c-accordion-content="" |
311
|
|
|
data-c-background="gray(10)" |
312
|
|
|
> |
313
|
|
|
<Form> |
314
|
|
|
<AlertWhenUnsaved /> |
315
|
|
|
<hr data-c-hr="thin(gray)" data-c-margin="bottom(1)" /> |
316
|
|
|
<div data-c-padding="lr(1)"> |
317
|
|
|
<FastField |
318
|
|
|
id={`experience-skill-textarea-${experienceSkill.experience_type}-${experienceSkill.skill_id}-${experienceSkill.experience_id}`} |
319
|
|
|
name="justification" |
320
|
|
|
label={label} |
321
|
|
|
component={TextAreaInput} |
322
|
|
|
placeholder="Start writing here..." |
323
|
|
|
required |
324
|
|
|
/> |
325
|
|
|
</div> |
326
|
|
|
<div data-c-padding="all(1)"> |
327
|
|
|
<div data-c-grid="gutter(all, 1) middle"> |
328
|
|
|
<div |
329
|
|
|
data-c-grid-item="tp(1of2)" |
330
|
|
|
data-c-align="base(center) tp(left)" |
331
|
|
|
> |
332
|
|
|
<button |
333
|
|
|
data-c-button="outline(c1)" |
334
|
|
|
data-c-radius="rounded" |
335
|
|
|
data-c-dialog-id="confirm-deletion" |
336
|
|
|
type="button" |
337
|
|
|
data-c-dialog-action="open" |
338
|
|
|
> |
339
|
|
|
<span> |
340
|
|
|
<FormattedMessage |
341
|
|
|
id="application.skills.deleteExperienceButtonText" |
342
|
|
|
defaultMessage="Remove Experience From Skill" |
343
|
|
|
description="Text for the delete experience button." |
344
|
|
|
/> |
345
|
|
|
</span> |
346
|
|
|
</button> |
347
|
|
|
</div> |
348
|
|
|
<div |
349
|
|
|
data-c-grid-item="tp(1of2)" |
350
|
|
|
data-c-align="base(center) tp(right)" |
351
|
|
|
> |
352
|
|
|
<WordCounter |
353
|
|
|
elementId={`experience-skill-textarea-${experienceSkill.experience_type}-${experienceSkill.skill_id}-${experienceSkill.experience_id}`} |
354
|
|
|
maxWords={JUSTIFICATION_WORD_LIMIT} |
355
|
|
|
minWords={0} |
356
|
|
|
absoluteValue |
357
|
|
|
dataAttributes={{ "data-c-margin": "right(1)" }} |
358
|
|
|
underMaxMessage={intl.formatMessage( |
359
|
|
|
displayMessages.wordCountUnderMax, |
360
|
|
|
)} |
361
|
|
|
overMaxMessage={intl.formatMessage( |
362
|
|
|
displayMessages.wordCountOverMax, |
363
|
|
|
)} |
364
|
|
|
/> |
365
|
|
|
<button |
366
|
|
|
data-c-button="solid(c1)" |
367
|
|
|
data-c-radius="rounded" |
368
|
|
|
type="submit" |
369
|
|
|
disabled={!dirty || isSubmitting} |
370
|
|
|
> |
371
|
|
|
<span> |
372
|
|
|
{dirty |
373
|
|
|
? intl.formatMessage(displayMessages.save) |
374
|
|
|
: intl.formatMessage(displayMessages.saved)} |
375
|
|
|
</span> |
376
|
|
|
</button> |
377
|
|
|
</div> |
378
|
|
|
</div> |
379
|
|
|
</div> |
380
|
|
|
</Form> |
381
|
|
|
</div> |
382
|
|
|
)} |
383
|
|
|
</div> |
384
|
|
|
)} |
385
|
|
|
</Formik> |
386
|
|
|
); |
387
|
|
|
}; |
388
|
|
|
|
389
|
|
|
interface SkillsProps { |
390
|
|
|
criteria: Criteria[]; |
391
|
|
|
experiences: ExperienceSkill[]; |
392
|
|
|
skills: Skill[]; |
393
|
|
|
handleUpdateExperienceJustification: ( |
394
|
|
|
experience: ExperienceSkill, |
395
|
|
|
) => Promise<ExperienceSkill>; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
const Skills: React.FC<SkillsProps> = ({ |
399
|
|
|
criteria, |
400
|
|
|
experiences, |
401
|
|
|
skills, |
402
|
|
|
handleUpdateExperienceJustification, |
403
|
|
|
}) => { |
404
|
|
|
const intl = useIntl(); |
405
|
|
|
const locale = getLocale(intl.locale); |
406
|
|
|
|
407
|
|
|
const skillsById = mapToObject(skills, getId); |
408
|
|
|
const getSkillOfCriteria = (criterion: Criteria): Skill | null => { |
409
|
|
|
return hasKey(skillsById, criterion.skill_id) |
410
|
|
|
? skillsById[criterion.skill_id] |
411
|
|
|
: null; |
412
|
|
|
}; |
413
|
|
|
|
414
|
|
|
const getExperiencesOfSkill = (skill: Skill): ExperienceSkill[] => |
415
|
|
|
experiences.filter((experience) => experience.skill_id === skill.id); |
416
|
|
|
|
417
|
|
|
const menuSkills = criteria.flatMap((criterion: Criteria) => { |
418
|
|
|
const skill = getSkillOfCriteria(criterion); |
419
|
|
|
if (skill) { |
420
|
|
|
return [localizeFieldNonNull(locale, skill, "name")]; |
421
|
|
|
} |
422
|
|
|
return []; |
423
|
|
|
}); |
424
|
|
|
|
425
|
|
|
const [experienceSkillStatus, setExperienceSkillStatus] = useState( |
426
|
|
|
Object.fromEntries( |
427
|
|
|
menuSkills.map((menuSkill) => [slugify(menuSkill), IconStatus.DEFAULT]), |
428
|
|
|
), |
429
|
|
|
); |
430
|
|
|
|
431
|
|
|
return ( |
432
|
|
|
<div data-c-container="large"> |
433
|
|
|
<div data-c-grid="gutter(all, 1)"> |
434
|
|
|
<div data-c-grid-item="tl(1of4)"> |
435
|
|
|
<Sidebar |
436
|
|
|
menuSkills={menuSkills} |
437
|
|
|
intl={intl} |
438
|
|
|
status={experienceSkillStatus} |
439
|
|
|
/> |
440
|
|
|
</div> |
441
|
|
|
<div data-c-grid-item="tl(3of4)"> |
442
|
|
|
<h2 data-c-heading="h2" data-c-margin="top(3) bottom(1)"> |
443
|
|
|
<FormattedMessage |
444
|
|
|
id="application.skills.heading" |
445
|
|
|
defaultMessage="How You Used Each Skill" |
446
|
|
|
description="Heading text on the Skills step." |
447
|
|
|
/> |
448
|
|
|
</h2> |
449
|
|
|
<p data-c-margin="bottom(1)"> |
450
|
|
|
<span data-c-font-weight="bold"> |
451
|
|
|
This is the most important part of your application. |
452
|
|
|
</span>{" "} |
453
|
|
|
Each box only needs a couple of sentences, but make them good ones! |
454
|
|
|
</p> |
455
|
|
|
<p data-c-margin="bottom(.5)"> |
456
|
|
|
Try answering one or two of the following questions: |
457
|
|
|
</p> |
458
|
|
|
<ul data-c-margin="bottom(1)"> |
459
|
|
|
<li> |
460
|
|
|
What did you accomplish, create, or deliver using this skill? |
461
|
|
|
</li> |
462
|
|
|
<li> |
463
|
|
|
What tasks or activities did you do that relate to this skill? |
464
|
|
|
</li> |
465
|
|
|
<li> |
466
|
|
|
Were there any special techniques or approaches that you used? |
467
|
|
|
</li> |
468
|
|
|
<li>How much responsibility did you have in this role?</li> |
469
|
|
|
</ul> |
470
|
|
|
<p> |
471
|
|
|
If a skill is only loosely connected to an experience, consider |
472
|
|
|
removing it. This can help the manager focus on your best examples. |
473
|
|
|
</p> |
474
|
|
|
<div className="skills-list"> |
475
|
|
|
{criteria.map((criterion) => { |
476
|
|
|
const skill = getSkillOfCriteria(criterion); |
477
|
|
|
if (skill === null) { |
478
|
|
|
return null; |
479
|
|
|
} |
480
|
|
|
const skillName = localizeFieldNonNull(locale, skill, "name"); |
481
|
|
|
const skillHtmlId = slugify(skillName); |
482
|
|
|
|
483
|
|
|
return ( |
484
|
|
|
<> |
485
|
|
|
<h3 |
486
|
|
|
className="application-skill-title" |
487
|
|
|
data-c-heading="h3" |
488
|
|
|
data-c-padding="top(3) bottom(1)" |
489
|
|
|
data-c-margin="bottom(1)" |
490
|
|
|
id={skillHtmlId} |
491
|
|
|
> |
492
|
|
|
<button |
493
|
|
|
data-c-font-size="h3" |
494
|
|
|
data-c-dialog-id="skill-description" |
495
|
|
|
type="button" |
496
|
|
|
data-c-dialog-action="open" |
497
|
|
|
> |
498
|
|
|
{skillName} |
499
|
|
|
</button> |
500
|
|
|
<br /> |
501
|
|
|
<button |
502
|
|
|
data-c-font-size="normal" |
503
|
|
|
data-c-font-weight="bold" |
504
|
|
|
data-c-dialog-id="level-description" |
505
|
|
|
type="button" |
506
|
|
|
data-c-dialog-action="open" |
507
|
|
|
> |
508
|
|
|
{intl.formatMessage(getSkillLevelName(criterion, skill))} |
509
|
|
|
</button> |
510
|
|
|
</h3> |
511
|
|
|
{getExperiencesOfSkill(skill).length === 0 ? ( |
512
|
|
|
<div |
513
|
|
|
data-c-background="gray(10)" |
514
|
|
|
data-c-radius="rounded" |
515
|
|
|
data-c-border="all(thin, solid, gray)" |
516
|
|
|
data-c-padding="all(1)" |
517
|
|
|
> |
518
|
|
|
<div data-c-align="base(center)"> |
519
|
|
|
<p data-c-color="gray"> |
520
|
|
|
<FormattedMessage |
521
|
|
|
id="application.skills.noLinkedExperiences" |
522
|
|
|
defaultMessage="Looks like you don't have any experiences linked to this skill. You can link experiences to skills in the previous step." |
523
|
|
|
description="Text displayed under a skill section with no experiences." |
524
|
|
|
/> |
525
|
|
|
</p> |
526
|
|
|
</div> |
527
|
|
|
</div> |
528
|
|
|
) : ( |
529
|
|
|
<div data-c-accordion-group=""> |
530
|
|
|
{getExperiencesOfSkill(skill).map((experienceSkill) => ( |
531
|
|
|
<ExperienceAccordion |
532
|
|
|
key={`experience-skill-textarea-${experienceSkill.experience_type}-${experienceSkill.skill_id}-${experienceSkill.experience_id}`} |
533
|
|
|
experienceSkill={experienceSkill} |
534
|
|
|
intl={intl} |
535
|
|
|
status={experienceSkillStatus} |
536
|
|
|
handleUpdateStatus={setExperienceSkillStatus} |
537
|
|
|
skillName={skillName} |
538
|
|
|
handleUpdateExperienceJustification={ |
539
|
|
|
handleUpdateExperienceJustification |
540
|
|
|
} |
541
|
|
|
/> |
542
|
|
|
))} |
543
|
|
|
</div> |
544
|
|
|
)} |
545
|
|
|
</> |
546
|
|
|
); |
547
|
|
|
})} |
548
|
|
|
</div> |
549
|
|
|
</div> |
550
|
|
|
</div> |
551
|
|
|
</div> |
552
|
|
|
); |
553
|
|
|
}; |
554
|
|
|
|
555
|
|
|
export default Skills; |
556
|
|
|
|