feat: remove portfolio and add conditional intro
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
3483a04ba1
commit
2352c0b1d9
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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', () => {
|
||||
|
@ -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>
|
||||
|
42
client/src/components/Intro/Intro.test.js
Normal file
42
client/src/components/Intro/Intro.test.js
Normal 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: ''
|
||||
};
|
130
client/src/components/Intro/index.js
Normal file
130
client/src/components/Intro/index.js
Normal 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;
|
49
client/src/components/Intro/intro.css
Normal file
49
client/src/components/Intro/intro.css
Normal 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;
|
||||
}
|
@ -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);
|
||||
|
@ -10,7 +10,7 @@ body {
|
||||
|
||||
.btn-cta-big {
|
||||
max-height: 100%;
|
||||
font-size: 40px;
|
||||
font-size: 27px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
|
@ -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 }] }
|
||||
}
|
||||
};
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
Reference in New Issue
Block a user