Skip to content

Commit

Permalink
feat: ENT-7367 Added skills quiz v2 (#911)
Browse files Browse the repository at this point in the history
* feat: Added skills quiz v2

* fix: Fixed duplicate components

* feat: unfold top skills and recommendations on card click

---------

Co-authored-by: IrfanUddinAhmad <irfanahmad@arbisoft.com>
Co-authored-by: mahamakifdar19 <maham.akif@arbisoft.com>
  • Loading branch information
3 people authored Jan 4, 2024
1 parent e2cdf97 commit 032fab7
Show file tree
Hide file tree
Showing 19 changed files with 695 additions and 82 deletions.
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 });
} else if (jobs?.length === 0) {
setJobSelected(undefined);
dispatch({ type: SET_KEY_VALUE, key: 'selectedJob', value: undefined });
}
}, [jobs, dispatch, jobSelected]);

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

return !isLoading ? (
<>
<SelectableBox.Set
type="radio"
value={jobSelected}
onChange={handleChange}
name="industry"
columns="3"
className="selectable-box mt-4"
>
{jobs.map((job) => (
<SelectableBox
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}>
<Chip>{skill.name}</Chip>
</div>
))}
</div>
</SelectableBox>
))}
</SelectableBox.Set>
{(jobSelected || goal === DROPDOWN_OPTION_IMPROVE_CURRENT_ROLE) && (
<>
<TopSkillsOverview index={jobIndex} />
<Stack gap={4}>
<SearchCourseCard index={courseIndex} />
<SearchProgramCard index={courseIndex} />
<SearchPathways index={courseIndex} />
</Stack>
<div className="text-center py-4">
{ !showMoreRecommendedCourses && (
<Button
variant="outline-primary"
onClick={() => setShowMoreRecommendedCourses(true)}
>
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';
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`);
};

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()}>
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)}
>
{!hide ? 'Hide advanced options' : 'Show advanced options'}
</Button>
{!hide && (
<div>
<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

0 comments on commit 032fab7

Please sign in to comment.