feat(client): add prev next btns to lessons (#35485)
* feat/add-prev-next-btns-to-lessons
redirect to learn instead of home
* fix/button-css-properties
* fix/change-buttons-to-suggested
* fix/add-tests+change-button-to-link
Co-authored-by: ojeytonwilliams ojeytonwilliams@gmail.com
* Revert "fix/add-tests+change-button-to-link"
This reverts commit ae26504d1d
.
* fix/add-tests+change-button-to-link
Co-authored-by: ojeytonwilliams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -164,6 +164,9 @@ export class BackEnd extends Component {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
output,
|
output,
|
||||||
|
pageContext: {
|
||||||
|
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
||||||
|
},
|
||||||
tests,
|
tests,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
@ -181,7 +184,14 @@ export class BackEnd extends Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ChallengeTitle>{blockNameTitle}</ChallengeTitle>
|
<ChallengeTitle
|
||||||
|
introPath={introPath}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
|
showPrevNextBtns={true}
|
||||||
|
>
|
||||||
|
{blockNameTitle}
|
||||||
|
</ChallengeTitle>
|
||||||
<ChallengeDescription
|
<ChallengeDescription
|
||||||
description={description}
|
description={description}
|
||||||
instructions={instructions}
|
instructions={instructions}
|
||||||
|
@ -66,9 +66,7 @@ const propTypes = {
|
|||||||
initTests: PropTypes.func.isRequired,
|
initTests: PropTypes.func.isRequired,
|
||||||
output: PropTypes.string,
|
output: PropTypes.string,
|
||||||
pageContext: PropTypes.shape({
|
pageContext: PropTypes.shape({
|
||||||
challengeMeta: PropTypes.shape({
|
challengeMeta: PropTypes.object
|
||||||
nextChallengePath: PropTypes.string
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
tests: PropTypes.arrayOf(
|
tests: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
@ -201,13 +199,22 @@ class ShowClassic extends Component {
|
|||||||
instructions
|
instructions
|
||||||
} = this.getChallenge();
|
} = this.getChallenge();
|
||||||
|
|
||||||
|
const {
|
||||||
|
introPath,
|
||||||
|
nextChallengePath,
|
||||||
|
prevChallengePath
|
||||||
|
} = this.props.pageContext.challengeMeta;
|
||||||
return (
|
return (
|
||||||
<SidePanel
|
<SidePanel
|
||||||
className='full-height'
|
className='full-height'
|
||||||
description={description}
|
description={description}
|
||||||
guideUrl={this.getGuideUrl()}
|
guideUrl={this.getGuideUrl()}
|
||||||
instructions={instructions}
|
instructions={instructions}
|
||||||
|
introPath={introPath}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
section={dasherize(blockName)}
|
section={dasherize(blockName)}
|
||||||
|
showPrevNextBtns={true}
|
||||||
showToolPanel={showToolPanel}
|
showToolPanel={showToolPanel}
|
||||||
title={this.getBlockNameTitle()}
|
title={this.getBlockNameTitle()}
|
||||||
videoUrl={this.getVideoUrl()}
|
videoUrl={this.getVideoUrl()}
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Link from '../../../components/helpers/Link';
|
||||||
|
|
||||||
|
import './challenge-title.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
children: PropTypes.string,
|
children: PropTypes.string,
|
||||||
isCompleted: PropTypes.bool
|
introPath: PropTypes.string,
|
||||||
|
isCompleted: PropTypes.bool,
|
||||||
|
nextChallengePath: PropTypes.string,
|
||||||
|
prevChallengePath: PropTypes.string,
|
||||||
|
showPrevNextBtns: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChallengeTitle({ children, isCompleted }) {
|
function ChallengeTitle({
|
||||||
|
children,
|
||||||
|
introPath,
|
||||||
|
isCompleted,
|
||||||
|
nextChallengePath,
|
||||||
|
prevChallengePath,
|
||||||
|
showPrevNextBtns
|
||||||
|
}) {
|
||||||
let icon = null;
|
let icon = null;
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
icon = (
|
icon = (
|
||||||
@ -15,10 +29,30 @@ function ChallengeTitle({ children, isCompleted }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
<div className='challenge-title-wrap'>
|
||||||
|
{showPrevNextBtns ? (
|
||||||
|
<Link
|
||||||
|
aria-label='Previous lesson'
|
||||||
|
className='btn-invert btn btn-primary'
|
||||||
|
to={prevChallengePath}
|
||||||
|
>
|
||||||
|
<
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
<h2 className='text-center challenge-title'>
|
<h2 className='text-center challenge-title'>
|
||||||
{children || 'Happy Coding!'}
|
{children || 'Happy Coding!'}
|
||||||
{icon}
|
{icon}
|
||||||
</h2>
|
</h2>
|
||||||
|
{showPrevNextBtns ? (
|
||||||
|
<Link
|
||||||
|
aria-label='Next lesson'
|
||||||
|
className='btn-invert btn btn-primary'
|
||||||
|
to={introPath ? introPath : nextChallengePath}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
/* global expect */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Enzyme, { shallow } from 'enzyme';
|
||||||
|
import Adapter from 'enzyme-adapter-react-16';
|
||||||
|
import Link from '../../../components/helpers/Link';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
import ChallengeTitle from './Challenge-Title';
|
||||||
|
|
||||||
|
Enzyme.configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
children: 'title text',
|
||||||
|
introPath: '/intro/path',
|
||||||
|
isCompleted: true,
|
||||||
|
nextChallengePath: '/next',
|
||||||
|
prevChallengePath: '/prev',
|
||||||
|
showPrevNextBtns: true
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<ChallengeTitle/>', () => {
|
||||||
|
it('renders 0 <Link/>s by default', () => {
|
||||||
|
const titleToRender = <ChallengeTitle />;
|
||||||
|
const title = shallow(titleToRender);
|
||||||
|
expect(title.find(Link).length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a previous and a next <Link/>', () => {
|
||||||
|
const titleToRender = <ChallengeTitle {...baseProps} />;
|
||||||
|
const title = shallow(titleToRender);
|
||||||
|
expect(title.find(Link).length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a link to the previous challenge', () => {
|
||||||
|
const titleToRender = <ChallengeTitle {...baseProps} />;
|
||||||
|
const title = shallow(titleToRender);
|
||||||
|
expect(
|
||||||
|
title
|
||||||
|
.find(Link)
|
||||||
|
.first()
|
||||||
|
.prop('to')
|
||||||
|
).toBe('/prev');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a link to the next introduction if there is one', () => {
|
||||||
|
const titleToRender = <ChallengeTitle {...baseProps} />;
|
||||||
|
const title = shallow(titleToRender);
|
||||||
|
expect(
|
||||||
|
title
|
||||||
|
.find(Link)
|
||||||
|
.last()
|
||||||
|
.prop('to')
|
||||||
|
).toBe('/intro/path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a link to the next challenge otherwise', () => {
|
||||||
|
const props = { ...baseProps, introPath: null };
|
||||||
|
const titleToRender = <ChallengeTitle {...props} />;
|
||||||
|
const title = shallow(titleToRender);
|
||||||
|
expect(
|
||||||
|
title
|
||||||
|
.find(Link)
|
||||||
|
.last()
|
||||||
|
.prop('to')
|
||||||
|
).toBe('/next');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
const tree = renderer.create(<ChallengeTitle {...baseProps} />).toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
@ -35,7 +35,11 @@ const propTypes = {
|
|||||||
guideUrl: PropTypes.string,
|
guideUrl: PropTypes.string,
|
||||||
initConsole: PropTypes.func.isRequired,
|
initConsole: PropTypes.func.isRequired,
|
||||||
instructions: PropTypes.string,
|
instructions: PropTypes.string,
|
||||||
|
introPath: PropTypes.string,
|
||||||
|
nextChallengePath: PropTypes.string,
|
||||||
|
prevChallengePath: PropTypes.string,
|
||||||
section: PropTypes.string,
|
section: PropTypes.string,
|
||||||
|
showPrevNextBtns: PropTypes.bool,
|
||||||
showToolPanel: PropTypes.bool,
|
showToolPanel: PropTypes.bool,
|
||||||
tests: PropTypes.arrayOf(PropTypes.object),
|
tests: PropTypes.arrayOf(PropTypes.object),
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
@ -64,9 +68,13 @@ export class SidePanel extends Component {
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
instructions,
|
instructions,
|
||||||
|
introPath,
|
||||||
guideUrl,
|
guideUrl,
|
||||||
|
nextChallengePath,
|
||||||
|
prevChallengePath,
|
||||||
tests,
|
tests,
|
||||||
section,
|
section,
|
||||||
|
showPrevNextBtns,
|
||||||
showToolPanel,
|
showToolPanel,
|
||||||
videoUrl
|
videoUrl
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@ -74,7 +82,14 @@ export class SidePanel extends Component {
|
|||||||
<div className='instructions-panel' role='complementary'>
|
<div className='instructions-panel' role='complementary'>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<div>
|
<div>
|
||||||
<ChallengeTitle>{title}</ChallengeTitle>
|
<ChallengeTitle
|
||||||
|
introPath={introPath}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
|
showPrevNextBtns={showPrevNextBtns}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</ChallengeTitle>
|
||||||
<ChallengeDescription
|
<ChallengeDescription
|
||||||
description={description}
|
description={description}
|
||||||
instructions={instructions}
|
instructions={instructions}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<ChallengeTitle/> renders correctly 1`] = `
|
||||||
|
<div
|
||||||
|
className="challenge-title-wrap"
|
||||||
|
>
|
||||||
|
<MockedLink
|
||||||
|
aria-label="Previous lesson"
|
||||||
|
className="btn-invert btn btn-primary"
|
||||||
|
to="/prev"
|
||||||
|
>
|
||||||
|
<
|
||||||
|
</MockedLink>
|
||||||
|
<h2
|
||||||
|
className="text-center challenge-title"
|
||||||
|
>
|
||||||
|
title text
|
||||||
|
<i
|
||||||
|
className="ion-checkmark-circled text-primary"
|
||||||
|
title="Completed"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<MockedLink
|
||||||
|
aria-label="Next lesson"
|
||||||
|
className="btn-invert btn btn-primary"
|
||||||
|
to="/intro/path"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
</MockedLink>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,14 @@
|
|||||||
|
.challenge-title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-title {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-title-wrap > a {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
@ -100,6 +100,9 @@ export class Project extends Component {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
openCompletionModal,
|
openCompletionModal,
|
||||||
|
pageContext: {
|
||||||
|
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
||||||
|
},
|
||||||
updateProjectFormValues
|
updateProjectFormValues
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const isFrontEnd = challengeType === frontEndProject;
|
const isFrontEnd = challengeType === frontEndProject;
|
||||||
@ -113,6 +116,10 @@ export class Project extends Component {
|
|||||||
className='full-height'
|
className='full-height'
|
||||||
description={description}
|
description={description}
|
||||||
guideUrl={guideUrl}
|
guideUrl={guideUrl}
|
||||||
|
introPath={introPath}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
|
showPrevNextBtns={true}
|
||||||
title={blockNameTitle}
|
title={blockNameTitle}
|
||||||
/>
|
/>
|
||||||
<ProjectForm
|
<ProjectForm
|
||||||
|
@ -5,16 +5,36 @@ import Spacer from '../../../components/helpers/Spacer';
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
|
introPath: PropTypes.string,
|
||||||
isCompleted: PropTypes.bool,
|
isCompleted: PropTypes.bool,
|
||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
|
nextChallengePath: PropTypes.string,
|
||||||
|
prevChallengePath: PropTypes.string,
|
||||||
|
showPrevNextBtns: PropTypes.bool,
|
||||||
title: PropTypes.string
|
title: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SidePanel({ title, description, isCompleted }) {
|
export default function SidePanel({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
introPath,
|
||||||
|
isCompleted,
|
||||||
|
nextChallengePath,
|
||||||
|
prevChallengePath,
|
||||||
|
showPrevNextBtns
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ChallengeTitle isCompleted={isCompleted}>{title}</ChallengeTitle>
|
<ChallengeTitle
|
||||||
|
introPath={introPath}
|
||||||
|
isCompleted={isCompleted}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
|
showPrevNextBtns={showPrevNextBtns}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</ChallengeTitle>
|
||||||
<div dangerouslySetInnerHTML={{ __html: description }} />
|
<div dangerouslySetInnerHTML={{ __html: description }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -25,6 +25,7 @@ const initialState = {
|
|||||||
challengeMeta: {
|
challengeMeta: {
|
||||||
id: '',
|
id: '',
|
||||||
nextChallengePath: '/',
|
nextChallengePath: '/',
|
||||||
|
prevChallengePath: '/',
|
||||||
introPath: '',
|
introPath: '',
|
||||||
challengeType: -1
|
challengeType: -1
|
||||||
},
|
},
|
||||||
|
@ -34,8 +34,14 @@ const views = {
|
|||||||
|
|
||||||
const getNextChallengePath = (node, index, nodeArray) => {
|
const getNextChallengePath = (node, index, nodeArray) => {
|
||||||
const next = nodeArray[index + 1];
|
const next = nodeArray[index + 1];
|
||||||
return next ? next.node.fields.slug : '/';
|
return next ? next.node.fields.slug : '/learn';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPrevChallengePath = (node, index, nodeArray) => {
|
||||||
|
const prev = nodeArray[index - 1];
|
||||||
|
return prev ? prev.node.fields.slug : '/learn';
|
||||||
|
};
|
||||||
|
|
||||||
const getTemplateComponent = challengeType => views[viewTypes[challengeType]];
|
const getTemplateComponent = challengeType => views[viewTypes[challengeType]];
|
||||||
|
|
||||||
const getIntroIfRequired = (node, index, nodeArray) => {
|
const getIntroIfRequired = (node, index, nodeArray) => {
|
||||||
@ -74,6 +80,7 @@ exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
|
|||||||
template,
|
template,
|
||||||
required,
|
required,
|
||||||
nextChallengePath: getNextChallengePath(node, index, thisArray),
|
nextChallengePath: getNextChallengePath(node, index, thisArray),
|
||||||
|
prevChallengePath: getPrevChallengePath(node, index, thisArray),
|
||||||
id
|
id
|
||||||
},
|
},
|
||||||
slug
|
slug
|
||||||
|
Reference in New Issue
Block a user