feat: adding actions and reducer

This commit is contained in:
Maxwell Frank
2023-02-01 21:46:44 +00:00
parent 4ecdf583ea
commit 8f42e6fbfb
8 changed files with 236 additions and 65 deletions

View File

@@ -1,9 +1,26 @@
import {
SET_GOAL,
SET_CURRENT_JOB_TITLE,
ADD_CAREER_INTEREST,
REMOVE_CAREER_INTEREEST,
} from './constants';
// eslint-disable-next-line import/prefer-default-export
export const setGoal = (payload) => ({
type: SET_GOAL,
payload,
});
export const setCurrentJobTitle = (payload) => ({
type: SET_CURRENT_JOB_TITLE,
payload,
});
export const addCareerInterest = (payload) => ({
type: ADD_CAREER_INTEREST,
payload,
});
export const removeCareerInterest = (payload) => ({
type: REMOVE_CAREER_INTEREEST,
payload,
});

View File

@@ -1,2 +1,7 @@
// eslint-disable-next-line import/prefer-default-export
export const SET_GOAL = 'SET_GOAL';
export const SET_CURRENT_JOB_TITLE = 'SET_CURRENT_JOB_TITLE';
export const ADD_CAREER_INTEREST = 'ADD_CAREER_INTEREST';
export const REMOVE_CAREER_INTEREEST = 'REMOVE_CAREER_INTEREEST';
export const STEP1 = 'select-your-preferences';
export const STEP2 = 'review-your-results';

View File

@@ -1,5 +1,8 @@
import {
SET_GOAL,
SET_CURRENT_JOB_TITLE,
ADD_CAREER_INTEREST,
REMOVE_CAREER_INTEREEST,
} from './constants';
export function skillsReducer(state, action) {
@@ -9,6 +12,21 @@ export function skillsReducer(state, action) {
...state,
currentGoal: action.payload,
};
case SET_CURRENT_JOB_TITLE:
return {
...state,
currentJobTitle: action.payload,
};
case ADD_CAREER_INTEREST:
return {
...state,
careerInterests: [...state.careerInterests, action.payload],
};
case REMOVE_CAREER_INTEREEST:
return {
...state,
careerInterests: state.careerInterests.filter(interest => interest !== action.payload),
};
default:
return state;
}
@@ -16,6 +34,8 @@ export function skillsReducer(state, action) {
export const skillsInitialState = {
currentGoal: '',
currentJobTitle: '',
careerInterests: [],
};
export default skillsReducer;

View File

@@ -1,20 +1,60 @@
import { skillsReducer } from '../reducer';
import { skillsReducer, skillsInitialState } from '../reducer';
import {
SET_GOAL,
SET_CURRENT_JOB_TITLE,
ADD_CAREER_INTEREST,
REMOVE_CAREER_INTEREEST,
} from '../constants';
describe('skillsReducer', () => {
const testState = {
currentGoal: '',
};
const testState = skillsInitialState;
beforeEach(() => jest.resetModules());
it('does not remove present data when SET_GOAL action is dispatched', () => {
const newSkillsPayload = 'test-goal';
const returnedState = skillsReducer(testState, { type: SET_GOAL, payload: newSkillsPayload });
const newGoalPayload = 'test-goal';
const returnedState = skillsReducer(testState, { type: SET_GOAL, payload: newGoalPayload });
const finalState = {
...testState,
currentGoal: 'test-goal',
};
expect(returnedState).toEqual(finalState);
});
it('does not remove present data when SET_JOB_TITLE action is dispatched', () => {
const newJobTitlePayload = 'test-job-title';
const returnedState = skillsReducer(testState, { type: SET_CURRENT_JOB_TITLE, payload: newJobTitlePayload });
const finalState = {
...testState,
currentJobTitle: 'test-job-title',
};
expect(returnedState).toEqual(finalState);
});
it('adds a careerInterest when ADD_CAREER_INTEREST action is dispatched', () => {
const newCareerInterestPayload = 'test-career-interest';
const returnedState = skillsReducer(testState, { type: ADD_CAREER_INTEREST, payload: newCareerInterestPayload });
const finalState = {
...testState,
careerInterests: [...testState.careerInterests, 'test-career-interest'],
};
expect(returnedState).toEqual(finalState);
});
it('removes a careerInterest when REMOVE_CAREER_INTEREST action is dispatched', () => {
const newCareerInterestPayload = 'test-career-interest';
const testStateWithInterest = {
...testState,
careerInterests: [newCareerInterestPayload],
};
const returnedState = skillsReducer(
testStateWithInterest,
{ type: REMOVE_CAREER_INTEREEST, payload: newCareerInterestPayload },
);
const finalState = {
...testStateWithInterest,
// override the 'careerInterests` field and remove 'test-career-interest' from the array
careerInterests: testStateWithInterest.careerInterests.filter(interest => interest !== newCareerInterestPayload),
};
expect(returnedState).toEqual(finalState);
});
});

View File

@@ -0,0 +1,63 @@
import React, { useContext } from 'react';
import {
Button,
} from '@edx/paragon';
import {
setGoal,
setCurrentJobTitle,
addCareerInterest,
removeCareerInterest,
} from '../data/actions';
import { SkillsBuilderContext } from '../skills-builder-context';
import { useAlgoliaSearch } from '../utils/hooks';
const SelectPreferences = () => {
// TODO: Temporarily disable the no-unused-vars check, we'll see these later
// eslint-disable-next-line no-unused-vars
const [algoliaClient, productSearchIndex, jobSearchIndex] = useAlgoliaSearch();
const [{ currentGoal, currentJobTitle, careerInterests }, dispatch] = useContext(SkillsBuilderContext);
return (
<>
<div className="p-4">
<h3>Render Question 1</h3>
<Button onClick={() => dispatch(setGoal('learn new things'))}>
Answer question 1
</Button>
<p>Goal: {currentGoal}</p>
</div>
{currentGoal && (
<div className="p-4">
<h3>Render question 2</h3>
<Button onClick={() => dispatch(setCurrentJobTitle('Software Engineer'))}>
Answer question 2
</Button>
<p>Current Job Title: {currentJobTitle}</p>
</div>
)}
{currentJobTitle && (
<div className="p-4">
<h3>Render Question 3</h3>
<Button
onClick={() => dispatch(addCareerInterest(`Joining the circus ${Math.random().toFixed(2)}`))}
disabled={careerInterests.length >= 3}
>
Answer question 3
</Button>
<p>
Career Interests (click to remove):
{careerInterests.map(interest => (
<Button onClick={() => dispatch(removeCareerInterest(interest))}>
{interest}
</Button>
))}
</p>
</div>
)}
</>
);
};
export default SelectPreferences;

View File

@@ -1,79 +1,93 @@
import React, { useContext, useState } from 'react';
import React, { useState, useContext } from 'react';
import {
ActionRow,
Button,
Container,
Form,
Stepper,
ModalDialog,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { useHistory } from 'react-router';
import {
setGoal,
} from '../data/actions';
STEP1,
STEP2,
} from '../data/constants';
import messages from './messages';
import { SkillsBuilderContext } from '../skills-builder-context';
import { SkillsBuilderHeader } from '../skills-builder-header';
import SelectPreferences from './SelectPreferences';
import ViewResults from './ViewResults';
import headerImage from '../images/headerImage.png';
import { useAlgoliaSearch } from '../utils/hooks';
const SkillsBuilderModal = () => {
const [state, dispatch] = useContext(SkillsBuilderContext);
const [learnerGoal, setLearnerGoal] = useState('');
// TODO: Temporarily disable the no-unused-vars check, we'll see these later
// eslint-disable-next-line no-unused-vars
const [algoliaClient, productSearchIndex, jobSearchIndex] = useAlgoliaSearch();
const [{ careerInterests }] = useContext(SkillsBuilderContext);
const [currentStep, setCurrentStep] = useState(STEP1);
const history = useHistory();
const onCloseHandle = () => {
window.history.back();
history.goBack();
};
return (
<ModalDialog
title="Skills Builder"
size="fullscreen"
className="skills-builder-modal"
isOpen
onClose={onCloseHandle}
>
<ModalDialog.Hero>
<ModalDialog.Hero.Background className="bg-primary-500">
<img src={headerImage} alt="" className="h-100" />
</ModalDialog.Hero.Background>
<ModalDialog.Hero.Content>
<SkillsBuilderHeader />
</ModalDialog.Hero.Content>
</ModalDialog.Hero>
<ModalDialog.Body>
<Container>
<h3>Your current goal: {state.currentGoal}</h3>
<br />
<Form.Group controlId="currentLearnerGoal">
<Form.Control
type="text"
floatingLabel="Goal"
value={learnerGoal}
onChange={(e) => setLearnerGoal(e.target.value)}
/>
</Form.Group>
<Button onClick={() => dispatch(setGoal(learnerGoal))}>
Submit
</Button>
</Container>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<Button variant="tertiary">
<FormattedMessage {...messages.goBackButton} />
</Button>
<ActionRow.Spacer />
<Button>
<FormattedMessage {...messages.nextStepButton} />
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
<Stepper activeKey={currentStep}>
<ModalDialog
title="Skills Builder"
size="fullscreen"
className="skills-builder-modal"
isOpen
onClose={onCloseHandle}
>
<ModalDialog.Hero>
<ModalDialog.Hero.Background className="bg-primary-500">
<img src={headerImage} alt="" className="h-100" />
</ModalDialog.Hero.Background>
<ModalDialog.Hero.Content>
<SkillsBuilderHeader />
</ModalDialog.Hero.Content>
</ModalDialog.Hero>
<ModalDialog.Body>
<Container size="lg">
<Stepper.Step eventKey={STEP1} title="Select preferences">
<SelectPreferences />
</Stepper.Step>
<Stepper.Step eventKey={STEP2} title="Review results">
<ViewResults />
</Stepper.Step>
</Container>
</ModalDialog.Body>
<ModalDialog.Footer>
<Stepper.ActionRow eventKey={STEP1}>
<Button variant="outline-primary">
<FormattedMessage {...messages.goBackButton} />
</Button>
<Stepper.ActionRow.Spacer />
<Button
onClick={() => setCurrentStep(STEP2)}
disabled={careerInterests.length === 0}
>
<FormattedMessage {...messages.nextStepButton} />
</Button>
</Stepper.ActionRow>
<Stepper.ActionRow eventKey={STEP2}>
<Button
variant="outline-primary"
onClick={() => setCurrentStep(STEP1)}
>
<FormattedMessage {...messages.goBackButton} />
</Button>
<Stepper.ActionRow.Spacer />
<Button onClick={onCloseHandle}>
<FormattedMessage {...messages.exitButton} />
</Button>
</Stepper.ActionRow>
</ModalDialog.Footer>
</ModalDialog>
</Stepper>
);
};

View File

@@ -0,0 +1,7 @@
import React from 'react';
const ViewResults = () => (
<h3>Results will render on this step</h3>
);
export default ViewResults;

View File

@@ -12,6 +12,11 @@ const messages = defineMessages({
defaultMessage: 'Next Step',
description: 'Button that sends the user to the next step in the skills builder.',
},
exitButton: {
id: 'exit.button',
defaultMessage: 'Exit',
description: 'Button that exits the Skills Builder.',
},
});
export default messages;