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:
Tom
2019-07-18 04:46:00 -05:00
committed by mrugesh
parent eba8953037
commit 8757deb56c
11 changed files with 233 additions and 14 deletions

View File

@ -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}

View File

@ -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()}

View File

@ -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 (
<h2 className='text-center challenge-title'> <div className='challenge-title-wrap'>
{children || 'Happy Coding!'} {showPrevNextBtns ? (
{icon} <Link
</h2> aria-label='Previous lesson'
className='btn-invert btn btn-primary'
to={prevChallengePath}
>
&lt;
</Link>
) : null}
<h2 className='text-center challenge-title'>
{children || 'Happy Coding!'}
{icon}
</h2>
{showPrevNextBtns ? (
<Link
aria-label='Next lesson'
className='btn-invert btn btn-primary'
to={introPath ? introPath : nextChallengePath}
>
&gt;
</Link>
) : null}
</div>
); );
} }

View File

@ -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();
});
});

View File

@ -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}

View File

@ -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"
>
&lt;
</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"
>
&gt;
</MockedLink>
</div>
`;

View File

@ -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;
}

View File

@ -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

View File

@ -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>
); );

View File

@ -25,6 +25,7 @@ const initialState = {
challengeMeta: { challengeMeta: {
id: '', id: '',
nextChallengePath: '/', nextChallengePath: '/',
prevChallengePath: '/',
introPath: '', introPath: '',
challengeType: -1 challengeType: -1
}, },

View File

@ -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