-
+ {formatMessage(messages.jobTitlePrompt)}
-
+
-
-
+
+ {formatMessage(messages.studentCheckboxPrompt)}
-
-
+
+ {formatMessage(messages.currentlyLookingCheckboxPrompt)}
diff --git a/src/skills-builder/skills-builder-modal/select-preferences/SelectPreferences.jsx b/src/skills-builder/skills-builder-modal/select-preferences/SelectPreferences.jsx
index 7eee9a6..90f65a5 100644
--- a/src/skills-builder/skills-builder-modal/select-preferences/SelectPreferences.jsx
+++ b/src/skills-builder/skills-builder-modal/select-preferences/SelectPreferences.jsx
@@ -2,7 +2,7 @@ import React, { useContext } from 'react';
import {
Stack,
} from '@edx/paragon';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { SkillsBuilderContext } from '../../skills-builder-context';
import GoalSelect from './GoalSelect';
import JobTitleSelect from './JobTitleSelect';
@@ -10,13 +10,14 @@ import CareerInterestSelect from './CareerInterestSelect';
import messages from './messages';
const SelectPreferences = () => {
+ const { formatMessage } = useIntl();
const { state } = useContext(SkillsBuilderContext);
const { currentGoal, currentJobTitle } = state;
return (
- <>
-
-
+
+
+ {formatMessage(messages.skillsBuilderDescription)}
@@ -30,7 +31,7 @@ const SelectPreferences = () => {
)}
- >
+
);
};
diff --git a/src/skills-builder/skills-builder-modal/select-preferences/messages.js b/src/skills-builder/skills-builder-modal/select-preferences/messages.js
index 53e1551..5efbb80 100644
--- a/src/skills-builder/skills-builder-modal/select-preferences/messages.js
+++ b/src/skills-builder/skills-builder-modal/select-preferences/messages.js
@@ -61,6 +61,11 @@ const messages = defineMessages({
defaultMessage: 'What careers are you interested in?',
description: 'Prompts the user to select careers they are interested in pursuing.',
},
+ careerInterestInputPlaceholder: {
+ id: 'career.interest.input.placeholder',
+ defaultMessage: 'Select up to 3 new job titles',
+ description: 'Placeholder text for the career interest input control.',
+ },
removeCareerInterestButtonAltText: {
id: 'career.interest.remove.button.alt.text',
defaultMessage: 'Remove career interest: ',
diff --git a/src/skills-builder/skills-builder-modal/view-results/RelatedSkillsSelectableBoxSet.jsx b/src/skills-builder/skills-builder-modal/view-results/RelatedSkillsSelectableBoxSet.jsx
new file mode 100644
index 0000000..1da521c
--- /dev/null
+++ b/src/skills-builder/skills-builder-modal/view-results/RelatedSkillsSelectableBoxSet.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ SelectableBox, Chip, Stack, useMediaQuery, breakpoints,
+} from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+
+const RelatedSkillsSelectableBoxSet = ({ jobSkillsList, selectedJobTitle, onChange }) => {
+ const { formatMessage } = useIntl();
+ const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });
+
+ const renderTopFiveSkills = (skills) => {
+ const topFiveSkills = skills.sort((a, b) => b.significance - a.significance).slice(0, 5);
+ return (
+ topFiveSkills.map(skill => (
+
+ {skill.name}
+
+ ))
+ );
+ };
+
+ return (
+
+ {jobSkillsList.map(job => (
+
+ {job.name}
+
+ {formatMessage(messages.relatedSkillsHeading)}
+ {renderTopFiveSkills(job.skills)}
+
+
+ ))}
+
+ );
+};
+
+RelatedSkillsSelectableBoxSet.propTypes = {
+ jobSkillsList: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
+ selectedJobTitle: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
+
+export default RelatedSkillsSelectableBoxSet;
diff --git a/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx b/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx
index 3477aeb..3879fda 100644
--- a/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx
+++ b/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx
@@ -1,7 +1,82 @@
-import React from 'react';
+import React, { useContext, useEffect, useState } from 'react';
+import {
+ Stack, Row, Alert, Spinner,
+} from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { CheckCircle } from '@edx/paragon/icons';
+import { SkillsBuilderContext } from '../../skills-builder-context';
+import RelatedSkillsSelectableBoxSet from './RelatedSkillsSelectableBoxSet';
+import { searchJobs, getProductRecommendations } from '../../utils/search';
+import messages from './messages';
-const ViewResults = () => (
-
Results will render on this step
-);
+const ViewResults = () => {
+ const { formatMessage } = useIntl();
+ const { algolia, state } = useContext(SkillsBuilderContext);
+ const { jobSearchIndex, productSearchIndex } = algolia;
+ const { careerInterests } = state;
+
+ const [selectedJobTitle, setSelectedJobTitle] = useState('');
+ const [jobSkillsList, setJobSkillsList] = useState([]);
+ // eslint-disable-next-line no-unused-vars
+ const [courseRecommendations, setCourseRecommendations] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const getJobs = async () => {
+ // fetch list of jobs with related skills
+ const jobInfo = await searchJobs(jobSearchIndex, careerInterests);
+
+ // fetch course recommendations based on related skills for each job
+ const results = await Promise.all(jobInfo.map(async (job) => {
+ const formattedSkills = job.skills.map(skill => skill.name);
+
+ const recommendations = await getProductRecommendations(productSearchIndex, 'course', formattedSkills);
+
+ const data = {
+ id: job.id,
+ name: job.name,
+ recommendations,
+ };
+
+ return data;
+ }));
+
+ setJobSkillsList(jobInfo);
+ setSelectedJobTitle(jobInfo[0].name);
+ setCourseRecommendations(results);
+ setIsLoading(false);
+ };
+ getJobs();
+ }, [careerInterests, jobSearchIndex, productSearchIndex]);
+
+ return (
+ isLoading ? (
+
+
+
+ ) : (
+
+
+
+ {formatMessage(messages.matchesFoundSuccessAlert)}
+
+
+
+ setSelectedJobTitle(e.target.value)}
+ />
+
+ )
+ );
+};
export default ViewResults;
diff --git a/src/skills-builder/skills-builder-modal/view-results/messages.js b/src/skills-builder/skills-builder-modal/view-results/messages.js
new file mode 100644
index 0000000..0fc53ce
--- /dev/null
+++ b/src/skills-builder/skills-builder-modal/view-results/messages.js
@@ -0,0 +1,21 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ matchesFoundSuccessAlert: {
+ id: 'matches.found.success.alert',
+ defaultMessage: 'We found skills and courses that match your preferences!',
+ description: 'Success alert message to display when recommendations are presented to the learner.',
+ },
+ relatedSkillsHeading: {
+ id: 'related.skills.heading',
+ defaultMessage: 'Related Skills',
+ description: 'Heading text for a selectable box that displays related skills for a corresponding selected job title.',
+ },
+ relatedSkillsSelectableBoxLabelText: {
+ id: 'related.skills.selectable.box.label.text',
+ defaultMessage: 'Related skills:',
+ description: 'Label text for a selectable box that displays related skills for a corresponding selected job title.',
+ },
+});
+
+export default messages;
diff --git a/src/skills-builder/test/SkillsBuilder.test.jsx b/src/skills-builder/test/SkillsBuilder.test.jsx
index b9b8b7b..30ddd4f 100644
--- a/src/skills-builder/test/SkillsBuilder.test.jsx
+++ b/src/skills-builder/test/SkillsBuilder.test.jsx
@@ -8,15 +8,29 @@ import { SkillsBuilder } from '..';
import { SkillsBuilderModal } from '../skills-builder-modal';
import { SkillsBuilderProvider, SkillsBuilderContext } from '../skills-builder-context';
import { skillsInitialState } from '../data/reducer';
+import { mockData } from './__mocks__/jobSkills.mockData';
+import { getProductRecommendations, searchJobs, useAlgoliaSearch } from '../utils/search';
+
+const dispatchMock = jest.fn();
+
+jest.mock('@edx/frontend-platform/logging');
jest.mock('react-instantsearch-hooks-web', () => ({
// eslint-disable-next-line react/prop-types
InstantSearch: ({ children }) => ({children}
),
useSearchBox: jest.fn(() => ({ refine: jest.fn() })),
- useHits: jest.fn(() => ({ hits: [{ name: 'Text File Engineer' }, { name: 'Screen Viewer' }] })),
+ useHits: jest.fn(() => ({ hits: mockData.hits })),
}));
-const dispatchMock = jest.fn();
+jest.mock('../utils/search', () => ({
+ searchJobs: jest.fn(),
+ getProductRecommendations: jest.fn(),
+ useAlgoliaSearch: jest.fn(),
+}));
+
+searchJobs.mockReturnValue(mockData.searchJobs);
+getProductRecommendations.mockReturnValue(mockData.productRecommendations);
+useAlgoliaSearch.mockReturnValue(mockData.useAlgoliaSearch);
const contextValue = {
state: {
@@ -26,6 +40,8 @@ const contextValue = {
algolia: {
// Without this, tests would fail to destructure `searchClient` in the component
searchClient: {},
+ productSearchIndex: {},
+ jobSearchIndex: {},
},
};
@@ -76,7 +92,7 @@ describe('skills-builder', () => {
type: 'SET_GOAL',
};
const expectedJobTitle = {
- payload: 'student',
+ payload: 'Student',
type: 'SET_CURRENT_JOB_TITLE',
};
@@ -149,4 +165,29 @@ describe('skills-builder', () => {
fireEvent.click(screen.getByLabelText('Remove career interest: Prospector'));
expect(dispatchMock).toHaveBeenCalledWith(expected);
});
+
+ it('should render a for each career interest the learner has submitted', async () => {
+ render(
+ SkillsBuilderWrapperWithContext(
+ {
+ ...contextValue,
+ state: {
+ ...contextValue.state,
+ currentGoal: 'I want to start my career',
+ currentJobTitle: 'Goblin Lackey',
+ careerInterests: ['Prospector'],
+ },
+ },
+ ),
+ );
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: 'Next Step' }));
+ });
+
+ const chipComponents = document.querySelectorAll('.pgn__chip');
+ expect(chipComponents[0].textContent).toEqual('finding shiny things');
+ expect(chipComponents[1].textContent).toEqual('mining');
+
+ expect(screen.getByText('Prospector')).toBeTruthy();
+ });
});
diff --git a/src/skills-builder/test/__mocks__/jobSkills.mockData.js b/src/skills-builder/test/__mocks__/jobSkills.mockData.js
new file mode 100644
index 0000000..5150dec
--- /dev/null
+++ b/src/skills-builder/test/__mocks__/jobSkills.mockData.js
@@ -0,0 +1,40 @@
+export const mockData = {
+ hits: [
+ {
+ id: 0,
+ name: 'Text File Engineer'
+ },
+ {
+ id: 1,
+ name: 'Screen Viewer'
+ },
+ ],
+ searchJobs: [
+ {
+ id: 0,
+ name: 'Prospector',
+ skills: [
+ { id: 0,
+ name: 'mining',
+ significance: 50,
+ },
+ { id: 1,
+ name: 'finding shiny things',
+ significance: 100,
+ }],
+ },
+ ],
+ productRecommendations: [
+ {
+ id: 0,
+ name: 'Prospector',
+ recommendations: [{ name: 'Mining with the Mons' }, { name: 'The Art of Warren Upkeep' }],
+ },
+ {
+ id: 1,
+ name: 'Mirror Breaker',
+ recommendations: [{ name: 'Mirror Breaking 101' }],
+ },
+ ],
+ useAlgoliaSearch: [{}, {}, {}],
+};