Skip to content

Commit 09b62bc

Browse files
authored
Merge pull request freeCodeCamp#11322 from Bouncey/fix/Next-Step-Unlocked
Next step unlocked persistence
2 parents c7bebad + b9c7532 commit 09b62bc

File tree

6 files changed

+99
-39
lines changed

6 files changed

+99
-39
lines changed

common/app/routes/challenges/components/step/Step.jsx

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import PureComponent from 'react-pure-render/component';
66
import LightBox from 'react-images';
77

88
import {
9-
stepForward,
10-
stepBackward,
9+
closeLightBoxImage,
1110
completeAction,
12-
submitChallenge,
1311
openLightBoxImage,
14-
closeLightBoxImage
12+
stepBackward,
13+
stepForward,
14+
submitChallenge,
15+
updateUnlockedSteps
1516
} from '../../redux/actions';
1617
import { challengeSelector } from '../../redux/selectors';
1718
import { Button, Col, Image, Row } from 'react-bootstrap';
@@ -42,35 +43,37 @@ const mapStateToProps = createSelector(
4243
);
4344

4445
const dispatchActions = {
45-
stepForward,
46-
stepBackward,
46+
closeLightBoxImage,
4747
completeAction,
48-
submitChallenge,
4948
openLightBoxImage,
50-
closeLightBoxImage
49+
stepBackward,
50+
stepForward,
51+
submitChallenge,
52+
updateUnlockedSteps
53+
};
54+
55+
const propTypes = {
56+
closeLightBoxImage: PropTypes.func.isRequired,
57+
completeAction: PropTypes.func.isRequired,
58+
currentIndex: PropTypes.number,
59+
isActionCompleted: PropTypes.bool,
60+
isLastStep: PropTypes.bool,
61+
isLightBoxOpen: PropTypes.bool,
62+
numOfSteps: PropTypes.number,
63+
openLightBoxImage: PropTypes.func.isRequired,
64+
step: PropTypes.array,
65+
steps: PropTypes.array,
66+
stepBackward: PropTypes.func,
67+
stepForward: PropTypes.func,
68+
submitChallenge: PropTypes.func.isRequired,
69+
updateUnlockedSteps: PropTypes.func.isRequired
5170
};
5271

5372
export class StepChallenge extends PureComponent {
5473
constructor(...args) {
5574
super(...args);
5675
this.handleLightBoxOpen = this.handleLightBoxOpen.bind(this);
5776
}
58-
static displayName = 'StepChallenge';
59-
static propTypes = {
60-
currentIndex: PropTypes.number,
61-
step: PropTypes.array,
62-
steps: PropTypes.array,
63-
isActionCompleted: PropTypes.bool,
64-
isLastStep: PropTypes.bool,
65-
numOfSteps: PropTypes.number,
66-
stepForward: PropTypes.func,
67-
stepBackward: PropTypes.func,
68-
completeAction: PropTypes.func.isRequired,
69-
submitChallenge: PropTypes.func.isRequired,
70-
isLightBoxOpen: PropTypes.bool,
71-
openLightBoxImage: PropTypes.func.isRequired,
72-
closeLightBoxImage: PropTypes.func.isRequired
73-
};
7477

7578
handleLightBoxOpen(e) {
7679
if (!(e.ctrlKey || e.metaKey)) {
@@ -79,6 +82,23 @@ export class StepChallenge extends PureComponent {
7982
}
8083
}
8184

85+
componentWillMount() {
86+
const { updateUnlockedSteps } = this.props;
87+
updateUnlockedSteps([]);
88+
}
89+
90+
componentWillUnmount() {
91+
const { updateUnlockedSteps } = this.props;
92+
updateUnlockedSteps([]);
93+
}
94+
95+
componentWillReceiveProps(nextProps) {
96+
const { steps, updateUnlockedSteps } = this.props;
97+
if (nextProps.steps !== steps) {
98+
updateUnlockedSteps([]);
99+
}
100+
}
101+
82102
renderActionButton(action, completeAction) {
83103
const isApiAction = action === '#';
84104
const buttonCopy = isApiAction ?
@@ -260,4 +280,7 @@ export class StepChallenge extends PureComponent {
260280
}
261281
}
262282

283+
StepChallenge.displayName = 'StepChallenge';
284+
StepChallenge.propTypes = propTypes;
285+
263286
export default connect(mapStateToProps, dispatchActions)(StepChallenge);

common/app/routes/challenges/redux/actions.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import types from './types';
77
// step
88
export const stepForward = createAction(types.stepForward);
99
export const stepBackward = createAction(types.stepBackward);
10-
export const goToStep = createAction(types.goToStep);
10+
export const goToStep = createAction(
11+
types.goToStep,
12+
(step, isUnlocked) => ({ step, isUnlocked })
13+
);
1114
export const completeAction = createAction(types.completeAction);
15+
export const updateUnlockedSteps = createAction(types.updateUnlockedSteps);
1216
export const openLightBoxImage = createAction(types.openLightBoxImage);
1317
export const closeLightBoxImage = createAction(types.closeLightBoxImage);
1418

common/app/routes/challenges/redux/reducer.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ const initialUiState = {
4444
shouldShakeQuestion: false,
4545
shouldShowQuestions: false,
4646
isChallengeModalOpen: false,
47-
successMessage: 'Happy Coding!'
47+
successMessage: 'Happy Coding!',
48+
unlockedSteps: []
4849
};
4950
const initialState = {
5051
isCodeLocked: false,
@@ -143,17 +144,20 @@ const mainReducer = handleActions(
143144
}),
144145

145146
// step
146-
[types.goToStep]: (state, { payload: step = 0 }) => ({
147+
[types.goToStep]: (state, { payload: { step = 0, isUnlocked }}) => ({
147148
...state,
148149
currentIndex: step,
149150
previousIndex: state.currentIndex,
150-
isActionCompleted: false
151+
isActionCompleted: isUnlocked
151152
}),
152-
153153
[types.completeAction]: state => ({
154154
...state,
155155
isActionCompleted: true
156156
}),
157+
[types.updateUnlockedSteps]: (state, { payload }) => ({
158+
...state,
159+
unlockedSteps: payload
160+
}),
157161
[types.openLightBoxImage]: state => ({
158162
...state,
159163
isLightBoxOpen: true
Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,44 @@
11
import types from './types';
2-
import { goToStep, submitChallenge } from './actions';
2+
import { goToStep, submitChallenge, updateUnlockedSteps } from './actions';
33
import { challengeSelector } from './selectors';
44
import getActionsOfType from '../../../../utils/get-actions-of-type';
55

6+
function unlockStep(step, unlockedSteps) {
7+
if (!step) {
8+
return null;
9+
}
10+
const updatedSteps = [ ...unlockedSteps ];
11+
updatedSteps[step] = true;
12+
return updateUnlockedSteps(updatedSteps);
13+
}
14+
615
export default function stepChallengeEpic(actions, getState) {
716
return getActionsOfType(
817
actions,
918
types.stepForward,
10-
types.stepBackward
19+
types.stepBackward,
20+
types.completeAction
1121
)
1222
.map(({ type }) => {
1323
const state = getState();
1424
const { challenge: { description = [] } } = challengeSelector(state);
15-
const { challengesApp: { currentIndex } } = state;
25+
const { challengesApp: { currentIndex, unlockedSteps } } = state;
1626
const numOfSteps = description.length;
17-
const isLastStep = currentIndex + 1 >= numOfSteps;
27+
const stepFwd = currentIndex + 1;
28+
const stepBwd = currentIndex - 1;
29+
const isLastStep = stepFwd >= numOfSteps;
30+
if (type === types.completeAction) {
31+
return unlockStep(currentIndex, unlockedSteps);
32+
}
1833
if (type === types.stepForward) {
1934
if (isLastStep) {
2035
return submitChallenge();
2136
}
22-
return goToStep(currentIndex + 1);
37+
return goToStep(stepFwd, !!unlockedSteps[stepFwd]);
38+
}
39+
if (type === types.stepBackward) {
40+
return goToStep(stepBwd, !!unlockedSteps[stepBwd]);
2341
}
24-
return goToStep(currentIndex - 1);
42+
return null;
2543
});
2644
}

common/app/routes/challenges/redux/step-challenge-epic.test.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ test(file, function(t) {
3232
});
3333
t.test('steps back', t => {
3434
const actions = Observable.of({ type: types.stepBackward });
35-
const state = { challengesApp: { currentIndex: 1 } };
35+
const state = {
36+
challengesApp: {
37+
currentIndex: 1,
38+
unlockedSteps: [ true, undefined ] // eslint-disable-line no-undefined
39+
}
40+
};
3641
const onNextSpy = sinon.spy();
3742
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
3843
t.assert(_state === state, 'challenge selector not called with state');
@@ -56,7 +61,7 @@ test(file, function(t) {
5661
t.assert(
5762
onNextSpy.calledWithMatch({
5863
type: types.goToStep,
59-
payload: 0
64+
payload: { step: 0, isUnlocked: true }
6065
}),
6166
'Epic did not return the expected action'
6267
);
@@ -67,7 +72,12 @@ test(file, function(t) {
6772
});
6873
t.test('steps forward', t => {
6974
const actions = Observable.of({ type: types.stepForward });
70-
const state = { challengesApp: { currentIndex: 0 } };
75+
const state = {
76+
challengesApp: {
77+
currentIndex: 0,
78+
unlockedSteps: []
79+
}
80+
};
7181
const onNextSpy = sinon.spy();
7282
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
7383
t.assert(_state === state, 'challenge selector not called with state');
@@ -91,7 +101,7 @@ test(file, function(t) {
91101
t.assert(
92102
onNextSpy.calledWithMatch({
93103
type: types.goToStep,
94-
payload: 1
104+
payload: { step: 1, isUnlocked: false }
95105
}),
96106
'Epic did not return the expected action'
97107
);

common/app/routes/challenges/redux/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default createTypes([
88
'completeAction',
99
'openLightBoxImage',
1010
'closeLightBoxImage',
11+
'updateUnlockedSteps',
1112

1213
// challenges
1314
'fetchChallenge',

0 commit comments

Comments
 (0)