diff --git a/api-server/server/component-passport.js b/api-server/server/component-passport.js index ec6c667646..2283a50e9c 100644 --- a/api-server/server/component-passport.js +++ b/api-server/server/component-passport.js @@ -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); diff --git a/api-server/server/utils/middleware.js b/api-server/server/utils/middleware.js index da8469bc2e..db4e89c7f7 100644 --- a/api-server/server/utils/middleware.js +++ b/api-server/server/utils/middleware.js @@ -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); diff --git a/client/src/components/Header/Header.test.js b/client/src/components/Header/Header.test.js index 6f5852e7b7..b5a155a1f2 100644 --- a/client/src/components/Header/Header.test.js +++ b/client/src/components/Header/Header.test.js @@ -23,13 +23,13 @@ describe('', () => { 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', () => { diff --git a/client/src/components/Header/components/NavLinks.js b/client/src/components/Header/components/NavLinks.js index 54528cbe1f..f99f5ada5c 100644 --- a/client/src/components/Header/components/NavLinks.js +++ b/client/src/components/Header/components/NavLinks.js @@ -25,10 +25,7 @@ function NavLinks({ displayMenu }) {
  • - Projects -
  • -
  • - Portfolio + Learn
  • diff --git a/client/src/components/Intro/Intro.test.js b/client/src/components/Intro/Intro.test.js new file mode 100644 index 0000000000..d1708aa44b --- /dev/null +++ b/client/src/components/Intro/Intro.test.js @@ -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('', () => { + it('has no blockquotes when loggedOut', () => { + const container = renderer.create().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().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: '' +}; diff --git a/client/src/components/Intro/index.js b/client/src/components/Intro/index.js new file mode 100644 index 0000000000..cf1a3e9a19 --- /dev/null +++ b/client/src/components/Intro/index.js @@ -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 ( + <> + + + + + ); + } else if (isSignedIn) { + const { quote, author } = randomQuote(); + return ( + <> + + + +

    + {name + ? 'Welcome back, ' + name + '.' + : 'Welcome to freeCodeCamp.org'} +

    + + + + + +
    + + + +
    + + {quote} +
    + {author} +
    +
    +
    + +
    + + + +

    + If you are new to coding, we recommend you{' '} + start at the beginning. +

    + +
    + + ); + } else { + return ( + <> + + + +

    + Welcome to freeCodeCamp.org +

    + +

    Learn to code.

    +

    Build projects.

    +

    Earn certifications.

    +

    + Since 2014, more than 40,000 freeCodeCamp.org graduates have + gotten jobs at tech companies including: +

    +
    +

    Apple

    +

    Google

    +

    Amazon

    +

    Microsoft

    +

    Spotify

    +
    + + + + +
    + + + ); + } +} + +Intro.propTypes = propTypes; +Intro.displayName = 'Intro'; + +export default Intro; diff --git a/client/src/components/Intro/intro.css b/client/src/components/Intro/intro.css new file mode 100644 index 0000000000..fc39cbbcdc --- /dev/null +++ b/client/src/components/Intro/intro.css @@ -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; +} diff --git a/client/src/components/Map/index.js b/client/src/components/Map/index.js index be17e71271..de1c165d5c 100644 --- a/client/src/components/Map/index.js +++ b/client/src/components/Map/index.js @@ -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); } - // 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); + // 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) node = nodes[0]; + if (!node) return; toggleBlock(node.block); toggleSuperBlock(node.superBlock); diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css index 6d9bd6e807..4765b0352a 100644 --- a/client/src/components/layouts/global.css +++ b/client/src/components/layouts/global.css @@ -10,7 +10,7 @@ body { .btn-cta-big { max-height: 100%; - font-size: 40px; + font-size: 27px; white-space: normal; } diff --git a/client/src/components/welcome/Welcome.test.js b/client/src/components/welcome/Welcome.test.js deleted file mode 100644 index a7c985f134..0000000000 --- a/client/src/components/welcome/Welcome.test.js +++ /dev/null @@ -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('', () => { - it('renders when visiting index page and logged in', () => { - const shallow = new ShallowRenderer(); - shallow.render(); - const result = shallow.getRenderOutput(); - expect( - result.type.WrappedComponent.displayName === 'LearnLayout' - ).toBeTruthy(); - }); - - it('has a header', () => { - const container = renderer.create() - .root; - expect(container.findAllByType('h1').length === 1).toBeTruthy(); - }); - - it('has a blockquote', () => { - const container = renderer.create() - .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 }] } - } -}; diff --git a/client/src/components/welcome/index.js b/client/src/components/welcome/index.js deleted file mode 100644 index 6b18041469..0000000000 --- a/client/src/components/welcome/index.js +++ /dev/null @@ -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 ( - - - - -

    - {name - ? 'Welcome back, ' + name + '.' - : 'Welcome to freeCodeCamp.org'} -

    - -
    - - - -
    - - {quote} -
    - {author} -
    -
    -
    - -
    -
    - ); -} - -const propTypes = { - name: PropTypes.string -}; - -Welcome.propTypes = propTypes; -Welcome.displayName = 'Welcome'; - -export default Welcome; diff --git a/client/src/components/welcome/welcome.css b/client/src/components/welcome/welcome.css deleted file mode 100644 index 099a0eada3..0000000000 --- a/client/src/components/welcome/welcome.css +++ /dev/null @@ -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; -} diff --git a/client/src/pages/learn.js b/client/src/pages/learn.js index 8f1324b39b..d5646db551 100644 --- a/client/src/pages/learn.js +++ b/client/src/pages/learn.js @@ -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 ( - <> - - - - Sign in to save your progress. - - - - ); - } - 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 = ({ - - - - {BigCallToAction(isSignedIn)} - -

    - If you are new to coding, we recommend you{' '} - start at the beginning. -

    - -
    + 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 { diff --git a/client/src/pages/portfolio.js b/client/src/pages/portfolio.js deleted file mode 100644 index 5ac817b67d..0000000000 --- a/client/src/pages/portfolio.js +++ /dev/null @@ -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 ; - } - if (!showLoading && !isSignedIn) { - navigate(`${apiLocation}/signin?returnTo=portfolio`); - return ; - } - const RedirecUser = createRedirect('/' + username); - return ; -} - -ProfilePage.displayName = 'profilePage'; -ProfilePage.propTypes = propTypes; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(ProfilePage);