Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ENT-7367 Added skills quiz v2 #911

Merged
merged 3 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ SITE_NAME='edX'
ALGOLIA_APP_ID=''
ALGOLIA_SEARCH_API_KEY=''
ALGOLIA_INDEX_NAME=''
ALGOLIA_INDEX_NAME_JOBS=''
FULLSTORY_ORG_ID=''
USE_API_CACHE=''
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
Expand Down
124 changes: 124 additions & 0 deletions src/components/skills-quiz-v2/JobCardComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useContext, useState, useEffect } from 'react';
import {
SelectableBox, Chip, Spinner, Stack, Button,
} from '@edx/paragon';
import PropTypes from 'prop-types';
import { SkillsContext } from '../skills-quiz/SkillsContextProvider';
import { SET_KEY_VALUE } from '../skills-quiz/data/constants';
import { DROPDOWN_OPTION_IMPROVE_CURRENT_ROLE } from '../skills-quiz/constants';
import TopSkillsOverview from '../skills-quiz/TopSkillsOverview';
import SearchCourseCard from '../skills-quiz/SearchCourseCard';
import SearchProgramCard from '../skills-quiz/SearchProgramCard';
import SearchPathways from '../skills-quiz/SearchPathways';
import SkillsCourses from '../skills-quiz/SkillsCourses';

const JobCardComponent = ({
jobs, isLoading, jobIndex, courseIndex,
}) => {
const { dispatch, state } = useContext(SkillsContext);
const { goal } = state;
const [jobSelected, setJobSelected] = useState(undefined);
const [showMoreRecommendedCourses, setShowMoreRecommendedCourses] = useState(false);

useEffect(() => {
if (jobs?.length === 1) {
setJobSelected(jobs[0]?.name);
dispatch({ type: SET_KEY_VALUE, key: 'selectedJob', value: jobSelected });

Check warning on line 26 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L25-L26

Added lines #L25 - L26 were not covered by tests
} else if (jobs?.length === 0) {
setJobSelected(undefined);
dispatch({ type: SET_KEY_VALUE, key: 'selectedJob', value: undefined });

Check warning on line 29 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L28-L29

Added lines #L28 - L29 were not covered by tests
}
}, [jobs, dispatch, jobSelected]);

const handleChange = (e) => {
setJobSelected(e.target.value);
dispatch({ type: SET_KEY_VALUE, key: 'selectedJob', value: e.target.value });
setShowMoreRecommendedCourses(false);

Check warning on line 36 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L34-L36

Added lines #L34 - L36 were not covered by tests
};

return !isLoading ? (
<>

Check warning on line 40 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L40

Added line #L40 was not covered by tests
<SelectableBox.Set
type="radio"
value={jobSelected}
onChange={handleChange}
name="industry"
columns="3"
className="selectable-box mt-4"
>
{jobs.map((job) => (
<SelectableBox

Check warning on line 50 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L50

Added line #L50 was not covered by tests
key={job.id}
className="box"
value={job.name}
inputHidden={false}
type="radio"
aria-label={job.name}
isLoading={isLoading}
>
<div>
<div className="lead">{job.name}</div>
<div className="x-small mt-3">Related skills</div>
{job.skills.slice(0, 5).map((skill) => (
<div key={skill.name}>

Check warning on line 63 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L63

Added line #L63 was not covered by tests
<Chip>{skill.name}</Chip>
</div>
))}
</div>
</SelectableBox>
))}
</SelectableBox.Set>
{(jobSelected || goal === DROPDOWN_OPTION_IMPROVE_CURRENT_ROLE) && (
<>

Check warning on line 72 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L72

Added line #L72 was not covered by tests
<TopSkillsOverview index={jobIndex} />
<Stack gap={4}>
<SearchCourseCard index={courseIndex} />
<SearchProgramCard index={courseIndex} />
<SearchPathways index={courseIndex} />
</Stack>
<div className="text-center py-4">
{ !showMoreRecommendedCourses && (
<Button

Check warning on line 81 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L81

Added line #L81 was not covered by tests
variant="outline-primary"
onClick={() => setShowMoreRecommendedCourses(true)}

Check warning on line 83 in src/components/skills-quiz-v2/JobCardComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/JobCardComponent.jsx#L83

Added line #L83 was not covered by tests
>
See more course recommendations
</Button>
) }
</div>
{ showMoreRecommendedCourses && <SkillsCourses index={courseIndex} />}
</>
)}
</>
) : (
<Spinner
animation="border"
className="mie-3 d-block mt-4"
screenReaderText="loading"
/>
);
};

JobCardComponent.defaultProps = {
jobs: undefined,
isLoading: false,
jobIndex: undefined,
courseIndex: undefined,
};

JobCardComponent.propTypes = {
isLoading: PropTypes.bool,
jobs: PropTypes.arrayOf(PropTypes.shape()),
jobIndex: PropTypes.shape({
appId: PropTypes.string,
indexName: PropTypes.string,
search: PropTypes.func.isRequired,
}),
courseIndex: PropTypes.shape({
appId: PropTypes.string,
indexName: PropTypes.string,
search: PropTypes.func.isRequired,
}),
};

export default JobCardComponent;
35 changes: 35 additions & 0 deletions src/components/skills-quiz-v2/ProgramCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Card, useMediaQuery, breakpoints } from '@edx/paragon';
import PropTypes from 'prop-types';

const ProgramCard = ({
mainImg, logoImg, title, subtitle,
}) => {
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth });
return (
<Card isClickable style={{ width: isExtraSmall ? '100%' : '40%' }}>
<Card.ImageCap
src={mainImg}
srcAlt="Card image"
logoSrc={logoImg}
logoAlt="Card logo"
/>
<Card.Header title={title} subtitle={subtitle} />
</Card>
);
};

ProgramCard.propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
mainImg: PropTypes.string,
logoImg: PropTypes.string,
};

ProgramCard.defaultProps = {
title: null,
subtitle: null,
mainImg: null,
logoImg: null,
};

export default ProgramCard;
102 changes: 102 additions & 0 deletions src/components/skills-quiz-v2/SkillsQuiz.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Helmet } from 'react-helmet';
import './styles/index.scss';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[inform] Current convention in this MFE is to import component stylesheets within the top-level src/index.scss file as opposed to within the component's JS itself as done here.

Rationale: by importing through JS as done here, the SCSS cannot make use of the existing SCSS theme variables without importing duplicate variable definitions into this stylesheet first. By importing this SCSS file within src/index.scss instead, the skills quiz v2 SCSS can make use of Paragon theme variables.

For example, the styles for the original skills quiz is imported within src/index.scss here.

import { AppContext } from '@edx/frontend-platform/react';
import PropTypes from 'prop-types';
import {
ModalDialog, useToggle, ActionRow, Button,
} from '@edx/paragon';
import { useHistory } from 'react-router-dom';
import { useContext } from 'react';
import {
SKILL_BUILDER_TITLE,
text,
webTechBootCamps,
closeModalText,
} from './constants';
import ProgramCard from './ProgramCard';
import SkillsQuizHeader from './SkillsQuizHeader';
import SkillQuizForm from './SkillsQuizForm';
import headerImage from '../skills-quiz/images/headerImage.png';

const SkillsQuizV2 = ({ isStyleAutoSuggest }) => {
const { enterpriseConfig } = useContext(AppContext);
const history = useHistory();
const [isOpen, open, close] = useToggle(false);

const handleExit = () => {
history.push(`/${enterpriseConfig.slug}/search`);

Check warning on line 27 in src/components/skills-quiz-v2/SkillsQuiz.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/SkillsQuiz.jsx#L27

Added line #L27 was not covered by tests
};

const TITLE = `edx - ${SKILL_BUILDER_TITLE}`;
return (
<>
<Helmet title={TITLE} />
<ModalDialog
className="modal-small"
title="Close Dialog"
isOpen={isOpen}
onClose={close}
size="sm"
hasCloseButton={false}
>
<ModalDialog.Header>
<ModalDialog.Title>Exit Skill Builder?</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<p className="text-justify">{closeModalText}</p>
<ActionRow className="mt-4.5">
<Button variant="tertiary" onClick={close}>
Back to Skill Builder
</Button>
<Button variant="primary" onClick={() => handleExit()}>

Check warning on line 51 in src/components/skills-quiz-v2/SkillsQuiz.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/SkillsQuiz.jsx#L51

Added line #L51 was not covered by tests
Exit
</Button>
</ActionRow>
</ModalDialog.Body>
</ModalDialog>

<ModalDialog
title="Skills Quiz"
size="fullscreen"
className="bg-light-200 skills-quiz-modal"
isOpen
onClose={open}
>
<ModalDialog.Hero className="md-img">
<ModalDialog.Hero.Background backgroundSrc={headerImage} />
<ModalDialog.Hero.Content style={{ maxWidth: '15rem' }}>
<SkillsQuizHeader />
</ModalDialog.Hero.Content>
</ModalDialog.Hero>
<ModalDialog.Body>
<div className="page-body">
<div className="text">
<p className="text-gray-600 text-justify">{text}</p>
</div>
<SkillQuizForm isStyleAutoSuggest={isStyleAutoSuggest} />
<div className="cards-display">
<p className="pgn__form-label">
Boot camps for a web technology specialist
</p>
<div className="card-container">
{webTechBootCamps.map((bootcamp) => (
<ProgramCard {...bootcamp} />
))}
</div>
</div>
</div>
</ModalDialog.Body>
</ModalDialog>
</>
);
};

SkillsQuizV2.propTypes = {
isStyleAutoSuggest: PropTypes.bool,
};

SkillsQuizV2.defaultProps = {
isStyleAutoSuggest: false,
};

export default SkillsQuizV2;
84 changes: 84 additions & 0 deletions src/components/skills-quiz-v2/SkillsQuizForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Button } from '@edx/paragon';
import { useState, useMemo } from 'react';
import { getConfig } from '@edx/frontend-platform/config';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-dom';
import PropTypes from 'prop-types';
import SearchJobDropdown from '../skills-quiz/SearchJobDropdown';
import CurrentJobDropdown from '../skills-quiz/CurrentJobDropdown';
import IndustryDropdown from '../skills-quiz/IndustryDropdown';
import GoalDropdown from '../skills-quiz/GoalDropdown';
import SearchJobCard from '../skills-quiz/SearchJobCard';

const SkillQuizForm = ({ isStyleAutoSuggest }) => {
const config = getConfig();

const [searchClient, courseIndex, jobIndex] = useMemo(() => {
const client = algoliasearch(
config.ALGOLIA_APP_ID,
config.ALGOLIA_SEARCH_API_KEY,
);
const cIndex = client.initIndex(config.ALGOLIA_INDEX_NAME);
const jIndex = client.initIndex(config.ALGOLIA_INDEX_NAME_JOBS);
return [client, cIndex, jIndex];
}, [
config.ALGOLIA_APP_ID,
config.ALGOLIA_INDEX_NAME,
config.ALGOLIA_INDEX_NAME_JOBS,
config.ALGOLIA_SEARCH_API_KEY,
]);
const [hide, setHide] = useState(true);

return (
<div className="form">
<InstantSearch
indexName={config.ALGOLIA_INDEX_NAME_JOBS}
searchClient={searchClient}
>
<p className="mt-4 font-weight-bold">
What roles are you interested in ?
</p>
<SearchJobDropdown key="search" isStyleSearchBox isChip />
<Button
variant="link"
size="inline"
className="mb-2 mb-sm-0 btn"
onClick={() => setHide(!hide)}

Check warning on line 46 in src/components/skills-quiz-v2/SkillsQuizForm.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/SkillsQuizForm.jsx#L46

Added line #L46 was not covered by tests
>
{!hide ? 'Hide advanced options' : 'Show advanced options'}
</Button>
{!hide && (
<div>

Check warning on line 51 in src/components/skills-quiz-v2/SkillsQuizForm.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/skills-quiz-v2/SkillsQuizForm.jsx#L51

Added line #L51 was not covered by tests
<p className="mt-4 font-weight-bold">
Tell us about what you want to achieve ?
</p>
<GoalDropdown key="goal" />
<p className="mt-4 font-weight-bold">
Search and select your current job title
</p>
<CurrentJobDropdown
key="current"
isStyleAutoSuggest={isStyleAutoSuggest}
/>

<p className="mt-4 font-weight-bold">
What industry are you interested in ?
</p>
<IndustryDropdown key="industry" isStyleSearchBox />
</div>
)}
<SearchJobCard index={jobIndex} courseIndex={courseIndex} isSkillQuizV2 />
</InstantSearch>
</div>
);
};

SkillQuizForm.propTypes = {
isStyleAutoSuggest: PropTypes.bool,
};

SkillQuizForm.defaultProps = {
isStyleAutoSuggest: false,
};

export default SkillQuizForm;
15 changes: 15 additions & 0 deletions src/components/skills-quiz-v2/SkillsQuizHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import edxLogo from '../skills-quiz/images/edx-logo.svg';

const SkillsQuizHeader = () => (
<div style={{ display: 'flex' }} className="ml-2">
<img src={edxLogo} alt="edx-logo" height="110px" />
<div className="ml-5 vertical-line" />
<div style={{ minWidth: 'max-content' }} className="ml-5 header-text">
<h1 className="heading">Skills builder</h1>
<h1 className="subheading-v2 text-light-500">Let edX be your guide</h1>
</div>
</div>
);

export default SkillsQuizHeader;
23 changes: 23 additions & 0 deletions src/components/skills-quiz-v2/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const SKILL_BUILDER_TITLE = 'Skill Builder';

export const text = 'We combine the educational expertise with labor market data to help you reach your learning and professional goals. Whether you are looking to grow in your career, change careers, or just learn new skills, this tool can help you find a relevant course. Your role selection and recommendations are private and are not visible to your edX administrator';

export const webTechBootCamps = [
{
mainImg:
'https://prod-discovery.edx-cdn.org/media/course/image/8e119d15-b484-4de6-a795-b6d9be101233-19d5867a8db3.small.png',
logoImg:
'https://prod-discovery.edx-cdn.org/organization/logos/eac96c61-1462-4084-a0b2-12525b74a9e1-8377159ff774.png',
title: 'Engineering for Your Classroom K – 3',
subtitle: 'University of British Columbia',
},
{
mainImg:
'https://prod-discovery.edx-cdn.org/media/course/image/7868fb19-176b-4d98-b1a0-4d1e2029fdb8-b302dd3a98d1.jpg',
logoImg:
'https://prod-discovery.edx-cdn.org/organization/logos/eac96c61-1462-4084-a0b2-12525b74a9e1-8377159ff774.png',
title: 'Software Engineering : Introduction',
subtitle: 'University of British Columbia',
},
];
export const closeModalText = 'Learners who enroll in courses that align with their career goals are more likely to complete the course';
Loading
Loading