feat: remove portfolio and add conditional intro

This commit is contained in:
Ahmad Abdolsaheb
2019-10-22 13:38:19 +03:00
committed by Mrugesh Mohapatra
parent 3483a04ba1
commit 2352c0b1d9
14 changed files with 272 additions and 252 deletions

View File

@ -83,9 +83,9 @@ export const loginRedirect = () => {
const successRedirect = req => {
if (!!req && req.session && req.session.returnTo) {
delete req.session.returnTo;
return `${homeLocation}/`;
return `${homeLocation}/learn`;
}
return `${homeLocation}/`;
return `${homeLocation}/learn`;
};
let redirect = url.parse(successRedirect(req), true);

View File

@ -55,7 +55,7 @@ export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
return next();
}
export function ifUserRedirectTo(path = `${homeLocation}/`, status) {
export function ifUserRedirectTo(path = `${homeLocation}/learn`, status) {
status = status === 301 ? 301 : 302;
return (req, res, next) => {
const { accessToken } = getAccessTokenFromRequest(req);

View File

@ -23,13 +23,13 @@ describe('<NavLinks />', () => {
return acc;
}, []);
const expectedLinks = ['/learn', '/portfolio', '/news', '/forum'];
const expectedLinks = ['/learn', '/news', '/forum'];
it('renders to the DOM', () => {
expect(root).toBeTruthy();
});
it('has 2 links', () => {
expect(aTags.length === 4).toBeTruthy();
it('has 3 links', () => {
expect(aTags.length === 3).toBeTruthy();
});
it('has links to news, forum, learn and portfolio', () => {

View File

@ -25,10 +25,7 @@ function NavLinks({ displayMenu }) {
</Link>
</li>
<li className='nav-projects' role='menuitem'>
<Link to='/learn'>Projects</Link>
</li>
<li className='nav-portfolio' role='menuitem'>
<Link to='/portfolio'>Portfolio</Link>
<Link to='/learn'>Learn</Link>
</li>
</ul>
</div>

View File

@ -0,0 +1,42 @@
/* global expect */
import React from 'react';
import renderer from 'react-test-renderer';
// import ShallowRenderer from 'react-test-renderer/shallow';
import 'jest-dom/extend-expect';
import Intro from './';
describe('<Intro />', () => {
it('has no blockquotes when loggedOut', () => {
const container = renderer.create(<Intro {...loggedOutProps} />).root;
expect(container.findAllByType('blockquote').length === 0).toBeTruthy();
expect(container.findAllByType('h1').length === 1).toBeTruthy();
});
it('has a blockquote when loggedIn', () => {
const container = renderer.create(<Intro {...loggedInProps} />).root;
expect(container.findAllByType('blockquote').length === 1).toBeTruthy();
expect(container.findAllByType('h1').length === 1).toBeTruthy();
});
});
const loggedInProps = {
complete: true,
isSignedIn: true,
name: 'Developement User',
navigate: () => {},
pending: false,
slug: '/',
username: 'DevelopmentUser'
};
const loggedOutProps = {
complete: true,
isSignedIn: false,
name: '',
navigate: () => {},
pending: false,
slug: '/',
username: ''
};

View File

@ -0,0 +1,130 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link, Spacer, Loader } from '../helpers';
import { Row, Col } from '@freecodecamp/react-bootstrap';
import { navigate as gatsbyNavigate } from 'gatsby';
import { apiLocation } from '../../../config/env.json';
import { randomQuote } from '../../utils/get-words';
import './intro.css';
const propTypes = {
complete: PropTypes.bool,
isSignedIn: PropTypes.bool,
name: PropTypes.string,
navigate: PropTypes.func,
pending: PropTypes.bool,
slug: PropTypes.string,
username: PropTypes.string
};
function Intro({
isSignedIn,
username,
name,
navigate,
pending,
complete,
slug
}) {
if (pending && !complete) {
return (
<>
<Spacer />
<Loader />
<Spacer />
</>
);
} else if (isSignedIn) {
const { quote, author } = randomQuote();
return (
<>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<h1 className='text-center big-heading'>
{name
? 'Welcome back, ' + name + '.'
: 'Welcome to freeCodeCamp.org'}
</h1>
</Col>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<button
className={'btn-primary btn center-block'}
onClick={() => gatsbyNavigate(`/${username}`)}
>
View your Portfolio and Settings
</button>
</Col>
</Row>
<Spacer />
<Row className='text-center quote-partial'>
<Col sm={10} smOffset={1} xs={12}>
<blockquote className='blockquote'>
<span>
<q>{quote}</q>
<footer className='quote-author blockquote-footer'>
<cite>{author}</cite>
</footer>
</span>
</blockquote>
</Col>
</Row>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<h4>
If you are new to coding, we recommend you{' '}
<Link to={slug}>start at the beginning</Link>.
</h4>
</Col>
</Row>
</>
);
} else {
return (
<>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<h1 className='big-heading text-center'>
Welcome to freeCodeCamp.org
</h1>
<Spacer />
<h2 className='medium-heading'>Learn to code.</h2>
<h2 className='medium-heading'>Build projects.</h2>
<h2 className='medium-heading'>Earn certifications.</h2>
<h2 className='medium-heading'>
Since 2014, more than 40,000 freeCodeCamp.org graduates have
gotten jobs at tech companies including:
</h2>
<div className='logo-row'>
<h2 className='medium-heading'>Apple</h2>
<h2 className='medium-heading'>Google</h2>
<h2 className='medium-heading'>Amazon</h2>
<h2 className='medium-heading'>Microsoft</h2>
<h2 className='medium-heading'>Spotify</h2>
</div>
</Col>
<Col sm={10} smOffset={1} xs={12}>
<button
className={'btn-cta-big signup-btn btn-cta center-block'}
onClick={() => {
navigate(`${apiLocation}/signin`);
}}
>
Sign in to save your progress (it's free)
</button>
</Col>
</Row>
<Spacer />
</>
);
}
}
Intro.propTypes = propTypes;
Intro.displayName = 'Intro';
export default Intro;

View File

@ -0,0 +1,49 @@
.large-p {
font-size: 24px;
}
/* Buttons with a lot of text can overflow and mess up formatting on small
screens, this stops that unless the word itself is too large. */
.btn {
white-space: pre-line;
}
.logo-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
align-content: center;
}
.logo-row h2 {
height: 35px;
padding: 0 10px 0 10px;
}
.landing-page ul {
list-style: none;
padding-left: 0px;
}
#learn-app-wrapper h2.medium-heading {
margin-bottom: 50px;
margin-top: 0px;
text-align: center;
}
.quote-partial .blockquote {
font-size: 1.3rem;
border: none;
}
@media (max-width: 500px) {
.quote-partial .blockquote {
font-size: 1.2rem;
border: none;
}
}
.quote-author {
font-style: normal;
}

View File

@ -27,6 +27,7 @@ const propTypes = {
})
})
),
isSignedIn: PropTypes.bool,
nodes: PropTypes.arrayOf(ChallengeNode),
resetExpansion: PropTypes.func,
toggleBlock: PropTypes.func.isRequired,
@ -68,9 +69,11 @@ export class Map extends Component {
nodes,
resetExpansion,
toggleBlock,
toggleSuperBlock
toggleSuperBlock,
isSignedIn
} = this.props;
resetExpansion();
let node;
// find the challenge that has the same superblock with hash
@ -78,13 +81,17 @@ export class Map extends Component {
node = nodes.find(node => dasherize(node.superBlock) === hash);
}
// whitout hash only expand when signed in
if (isSignedIn) {
// if there is no hash or the hash did not match any challenge superblock
// and there was a currentChallengeId
if (!node && currentChallengeId) {
node = nodes.find(node => node.id === currentChallengeId);
}
if (!node) node = nodes[0];
}
if (!node) return;
toggleBlock(node.block);
toggleSuperBlock(node.superBlock);

View File

@ -10,7 +10,7 @@ body {
.btn-cta-big {
max-height: 100%;
font-size: 40px;
font-size: 27px;
white-space: normal;
}

View File

@ -1,63 +0,0 @@
/* global expect */
import React from 'react';
import renderer from 'react-test-renderer';
import ShallowRenderer from 'react-test-renderer/shallow';
import 'jest-dom/extend-expect';
import { LearnPage } from '../../pages/learn';
import Welcome from './';
import mockChallengeNodes from '../../__mocks__/challenge-nodes';
import mockIntroNodes from '../../__mocks__/intro-nodes';
describe('<Welcome />', () => {
it('renders when visiting index page and logged in', () => {
const shallow = new ShallowRenderer();
shallow.render(<LearnPage {...loggedInProps} />);
const result = shallow.getRenderOutput();
expect(
result.type.WrappedComponent.displayName === 'LearnLayout'
).toBeTruthy();
});
it('has a header', () => {
const container = renderer.create(<Welcome name={'Development User'} />)
.root;
expect(container.findAllByType('h1').length === 1).toBeTruthy();
});
it('has a blockquote', () => {
const container = renderer.create(<Welcome name={'Development User'} />)
.root;
expect(container.findAllByType('blockquote').length === 1).toBeTruthy();
});
});
const nodes = mockChallengeNodes.map(node => {
return { node };
});
const loggedInProps = {
fetchState: {
complete: true,
error: null,
errored: false,
pending: false
},
isSignedIn: true,
user: {
name: 'Development User'
},
location: { hash: '' },
data: {
challengeNode: {
fields: {
slug:
// eslint-disable-next-line max-len
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements'
}
},
allChallengeNode: { edges: nodes },
allMarkdownRemark: { edges: [{ mdEdges: mockIntroNodes }] }
}
};

View File

@ -1,52 +0,0 @@
import React, { Fragment } from 'react';
import { Row, Col } from '@freecodecamp/react-bootstrap';
import PropTypes from 'prop-types';
import { Spacer } from '../helpers';
import { randomQuote } from '../../utils/get-words';
import './welcome.css';
// created outside the function, so that the quotes don't change unless the
// page is reloaded.
const { quote, author } = randomQuote();
function Welcome({ name }) {
return (
<Fragment>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<h1 className='text-center big-heading'>
{name
? 'Welcome back, ' + name + '.'
: 'Welcome to freeCodeCamp.org'}
</h1>
</Col>
</Row>
<Spacer />
<Row className='text-center quote-partial'>
<Col sm={10} smOffset={1} xs={12}>
<blockquote className='blockquote'>
<span>
<q>{quote}</q>
<footer className='quote-author blockquote-footer'>
<cite>{author}</cite>
</footer>
</span>
</blockquote>
</Col>
</Row>
</Fragment>
);
}
const propTypes = {
name: PropTypes.string
};
Welcome.propTypes = propTypes;
Welcome.displayName = 'Welcome';
export default Welcome;

View File

@ -1,14 +0,0 @@
.quote-partial .blockquote {
font-size: 1.3rem;
border: none;
}
@media (max-width: 500px) {
.quote-partial .blockquote {
font-size: 1.2rem;
border: none;
}
}
.quote-author {
font-style: normal;
}

View File

@ -1,24 +1,21 @@
import React from 'react';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
import { Grid } from '@freecodecamp/react-bootstrap';
import PropTypes from 'prop-types';
import { createSelector } from 'reselect';
import { graphql } from 'gatsby';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import LearnLayout from '../components/layouts/Learn';
import { dasherize } from '../../../utils/slugs';
import Map from '../components/Map';
import Intro from '../components/Intro';
import {
userFetchStateSelector,
isSignedInSelector,
userSelector
userSelector,
hardGoTo as navigate
} from '../redux';
import LearnLayout from '../components/layouts/Learn';
import Login from '../components/Header/components/Login';
import { Link, Spacer } from '../components/helpers';
import Map from '../components/Map';
import Welcome from '../components/welcome';
import { dasherize } from '../../../utils/slugs';
import {
ChallengeNode,
AllChallengeNode,
@ -50,39 +47,30 @@ const propTypes = {
hash: PropTypes.string,
isSignedIn: PropTypes.bool,
location: PropTypes.object,
navigate: PropTypes.func.isRequired,
state: PropTypes.object,
user: PropTypes.shape({
name: PropTypes.string
name: PropTypes.string,
username: PropTypes.string
})
};
const BigCallToAction = isSignedIn => {
if (!isSignedIn) {
return (
<>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<Login>Sign in to save your progress.</Login>
</Col>
</Row>
</>
);
}
return '';
};
// choose between the state from landing page and hash from url.
const hashValueSelector = (state, hash) => {
if (state && state.superBlock) return dasherize(state.superBlock);
else if (hash) return hash.substr(1);
else return null;
};
const mapDispatchToProps = {
navigate
};
export const LearnPage = ({
location: { hash = '', state = '' },
isSignedIn,
user: { name = '' },
navigate,
fetchState: { pending, complete },
user: { name = '', username = '' },
data: {
challengeNode: {
fields: { slug }
@ -96,20 +84,19 @@ export const LearnPage = ({
<LearnLayout>
<Helmet title='Learn | freeCodeCamp.org' />
<Grid>
<Welcome name={name} />
<Row className='text-center'>
<Col sm={10} smOffset={1} xs={12}>
{BigCallToAction(isSignedIn)}
<Spacer />
<h3>
If you are new to coding, we recommend you{' '}
<Link to={slug}>start at the beginning</Link>.
</h3>
</Col>
</Row>
<Intro
complete={complete}
isSignedIn={isSignedIn}
name={name}
navigate={navigate}
pending={pending}
slug={slug}
username={username}
/>
<Map
hash={hashValue}
introNodes={mdEdges.map(({ node }) => node)}
isSignedIn={isSignedIn}
nodes={edges
.map(({ node }) => node)
.filter(({ isPrivate }) => !isPrivate)}
@ -122,7 +109,10 @@ export const LearnPage = ({
LearnPage.displayName = 'LearnPage';
LearnPage.propTypes = propTypes;
export default connect(mapStateToProps)(LearnPage);
export default connect(
mapStateToProps,
mapDispatchToProps
)(LearnPage);
export const query = graphql`
query FirstChallenge {

View File

@ -1,66 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PropTypes from 'prop-types';
import createRedirect from '../components/createRedirect';
import { apiLocation } from '../../config/env.json';
import {
signInLoadingSelector,
userSelector,
isSignedInSelector,
hardGoTo as navigate
} from '../redux';
import Loader from '../components/helpers/Loader';
const mapStateToProps = createSelector(
signInLoadingSelector,
userSelector,
isSignedInSelector,
(showLoading, user, isSignedIn) => ({
showLoading,
user,
isSignedIn
})
);
const mapDispatchToProps = {
navigate
};
const propTypes = {
isSignedIn: PropTypes.bool.isRequired,
navigate: PropTypes.func.isRequired,
showLoading: PropTypes.bool.isRequired,
user: PropTypes.shape({
username: PropTypes.string
})
};
function ProfilePage(props) {
const {
showLoading,
isSignedIn,
user: { username },
navigate
} = props;
if (showLoading) {
return <Loader fullScreen={true} />;
}
if (!showLoading && !isSignedIn) {
navigate(`${apiLocation}/signin?returnTo=portfolio`);
return <Loader fullScreen={true} />;
}
const RedirecUser = createRedirect('/' + username);
return <RedirecUser />;
}
ProfilePage.displayName = 'profilePage';
ProfilePage.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProfilePage);