feat: universal navbar (#36744)

* feat: add universal nav

* fix: add portfolio redirect
This commit is contained in:
Ahmad Abdolsaheb
2019-09-19 19:45:01 +03:00
committed by mrugesh
parent 5d946f3d77
commit e653235d94
24 changed files with 562 additions and 370 deletions

View File

@ -6,6 +6,7 @@ const gatsby = jest.requireActual('gatsby');
module.exports = { module.exports = {
...gatsby, ...gatsby,
navigate: jest.fn(),
graphql: jest.fn(), graphql: jest.fn(),
Link: jest.fn().mockImplementation( Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag // these props are invalid for an `a` tag

View File

@ -27,13 +27,13 @@ import Portfolio from '../components/settings/Portfolio';
import Honesty from '../components/settings/Honesty'; import Honesty from '../components/settings/Honesty';
import Certification from '../components/settings/Certification'; import Certification from '../components/settings/Certification';
import DangerZone from '../components/settings/DangerZone'; import DangerZone from '../components/settings/DangerZone';
import RedirectHome from '../components/RedirectHome';
const propTypes = { const propTypes = {
createFlashMessage: PropTypes.func.isRequired, createFlashMessage: PropTypes.func.isRequired,
hardGoTo: PropTypes.func.isRequired, hardGoTo: PropTypes.func.isRequired,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool.isRequired,
showLoading: PropTypes.bool, navigate: PropTypes.func.isRequired,
showLoading: PropTypes.bool.isRequired,
submitNewAbout: PropTypes.func.isRequired, submitNewAbout: PropTypes.func.isRequired,
toggleNightMode: PropTypes.func.isRequired, toggleNightMode: PropTypes.func.isRequired,
updateInternetSettings: PropTypes.func.isRequired, updateInternetSettings: PropTypes.func.isRequired,
@ -105,6 +105,7 @@ const mapDispatchToProps = dispatch =>
{ {
createFlashMessage, createFlashMessage,
hardGoTo, hardGoTo,
navigate: location => dispatch(hardGoTo(location)),
submitNewAbout, submitNewAbout,
toggleNightMode: theme => updateUserFlag({ theme }), toggleNightMode: theme => updateUserFlag({ theme }),
updateInternetSettings: updateUserFlag, updateInternetSettings: updateUserFlag,
@ -121,7 +122,7 @@ const createHandleSignoutClick = hardGoTo => e => {
return hardGoTo(`${apiLocation}/signout`); return hardGoTo(`${apiLocation}/signout`);
}; };
function ShowSettings(props) { export function ShowSettings(props) {
const { const {
createFlashMessage, createFlashMessage,
hardGoTo, hardGoTo,
@ -157,6 +158,7 @@ function ShowSettings(props) {
website, website,
portfolio portfolio
}, },
navigate,
showLoading, showLoading,
updateQuincyEmail, updateQuincyEmail,
updateInternetSettings, updateInternetSettings,
@ -170,7 +172,7 @@ function ShowSettings(props) {
} }
if (!showLoading && !isSignedIn) { if (!showLoading && !isSignedIn) {
return <RedirectHome />; return navigate(`${apiLocation}/signin`);
} }
return ( return (

View File

@ -0,0 +1,37 @@
/* global jest, expect */
import React from 'react';
import 'jest-dom/extend-expect';
import ShallowRenderer from 'react-test-renderer/shallow';
import { apiLocation } from '../../config/env.json';
import { ShowSettings } from './ShowSettings';
describe('<ShowSettings />', () => {
it('redirects to signin page when user not logged in', () => {
const shallow = new ShallowRenderer();
shallow.render(<ShowSettings {...loggedOutProps} />);
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(`${apiLocation}/signin`);
expect(true).toBeTruthy();
});
});
const navigate = jest.fn();
const loggedOutProps = {
createFlashMessage: jest.fn(),
hardGoTo: jest.fn(),
isSignedIn: false,
navigate: navigate,
showLoading: false,
submitNewAbout: jest.fn(),
toggleNightMode: jest.fn(),
updateInternetSettings: jest.fn(),
updateIsHonest: jest.fn(),
updatePortfolio: jest.fn(),
updateQuincyEmail: jest.fn(),
user: {
about: '',
completedChallenges: []
},
verifyCert: jest.fn()
};

View File

@ -0,0 +1,41 @@
/* global expect */
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import TestRenderer from 'react-test-renderer';
import Header from './';
import NavLinks from './components/NavLinks';
describe('<Header />', () => {
it('renders to the DOM', () => {
const shallow = new ShallowRenderer();
shallow.render(<Header />);
const result = shallow.getRenderOutput();
expect(result).toBeTruthy();
});
});
describe('<NavLinks />', () => {
const root = TestRenderer.create(<NavLinks />).root;
const aTags = root.findAllByType('a');
// reduces the aTags to href links
const links = aTags.reduce((acc, item) => {
acc.push(item._fiber.pendingProps.href);
return acc;
}, []);
const expectedLinks = ['/', '/portfolio'];
it('renders to the DOM', () => {
expect(root).toBeTruthy();
});
it('has 2 a tags', () => {
expect(aTags.length === 2).toBeTruthy();
});
it('has link to portfolio', () => {
// checks if all links in expected links exist in links
expect(expectedLinks.every(elem => links.indexOf(elem) > -1)).toBeTruthy();
});
});

View File

@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import './menuButton.css';
const MenuButton = React.forwardRef((props, ref) => (
<button
aria-expanded={props.displayMenu}
className={
'menu-button' +
(props.displayMenu ? ' menu-button-open' : '') +
' ' +
(props.className ? props.className : 'top-menu-button')
}
onClick={props.onClick}
ref={ref}
>
Menu
</button>
));
MenuButton.displayName = 'MenuButton';
MenuButton.propTypes = {
className: PropTypes.string,
displayMenu: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired
};
export default MenuButton;

View File

@ -1,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '../../helpers';
import UserState from '../components/UserState';
import './menuLinks.css';
function MenuLinks(props) {
return (
<ul className={props.className} id='top-right-nav'>
<li>
<Link className='top-right-nav-link' to='/learn'>
Learn
</Link>
</li>
<li>
<Link className='top-right-nav-link' external={true} to='/forum'>
Forum
</Link>
</li>
<li>
<Link className='top-right-nav-link' external={true} to='/news'>
News
</Link>
</li>
<li>
<UserState disableSettings={props.disableSettings} />
</li>
</ul>
);
}
MenuLinks.displayName = 'MenuLinks';
MenuLinks.propTypes = {
className: PropTypes.string,
disableSettings: PropTypes.bool
};
export default MenuLinks;

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Link } from '../../helpers';
export function NavLinks() {
return (
<div className='main-nav-group'>
<ul className={'nav-list display-flex'} role='menu'>
<li className='nav-theme' role='menuitem'>
<Link to='/'>Light</Link>
</li>
<li className='nav-portfolio' role='menuitem'>
<Link to='/portfolio'>Portfolio</Link>
</li>
</ul>
</div>
);
}
NavLinks.displayName = 'NavLinks';
export default NavLinks;

View File

@ -1,22 +1,18 @@
import React from 'react'; import React from 'react';
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg'; const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
const fCCglyph = 'https://s3.amazonaws.com/freecodecamp/FFCFire.png';
function NavLogo() { function NavLogo() {
return ( return (
<picture> <picture>
<source media='(max-width: 734px)' srcSet={fCCglyph} />
<source media='(min-width: 735px)' srcSet={fCClogo} />
<img <img
alt='learn to code at freeCodeCamp logo' alt='learn to code at freeCodeCamp logo'
className='nav-logo' className='nav-logo'
src={fCCglyph} src={fCClogo}
/> />
</picture> </picture>
); );
} }
NavLogo.displayName = 'NavLogo'; NavLogo.displayName = 'NavLogo';
export default NavLogo; export default NavLogo;

View File

@ -1,65 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import MenuButton from './MenuButton';
import MenuLinks from './MenuLinks';
class NavigationMenu extends Component {
constructor(props) {
super(props);
this.state = {
displayMenu: false
};
this.menuButtonRef = React.createRef();
this.handleClickOutside = this.handleClickOutside.bind(this);
this.toggleDisplayMenu = this.toggleDisplayMenu.bind(this);
}
componentDidMount() {
document.addEventListener('click', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside);
}
handleClickOutside(event) {
if (
this.state.displayMenu &&
this.menuButtonRef.current &&
!this.menuButtonRef.current.contains(event.target)
) {
this.toggleDisplayMenu();
}
}
toggleDisplayMenu() {
this.setState(({ displayMenu }) => ({ displayMenu: !displayMenu }));
}
render() {
const { disableSettings } = this.props;
const { displayMenu } = this.state;
return (
<>
<MenuButton
displayMenu={displayMenu}
onClick={this.toggleDisplayMenu}
ref={this.menuButtonRef}
/>
<MenuLinks
className={'top-nav' + (displayMenu ? ' top-nav-expanded' : '')}
disableSettings={disableSettings}
/>
</>
);
}
}
NavigationMenu.displayName = 'NavigationMenu';
NavigationMenu.propTypes = {
disableSettings: PropTypes.bool
};
export default NavigationMenu;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { Link } from '../../helpers';
import NavLogo from './NavLogo';
import SearchBar from '../../search/searchBar/SearchBar';
import NavLinks from './NavLinks';
import './universalNav.css';
export function UniversalNav() {
return (
<nav className={'universal-nav nav-padding'} id='universal-nav'>
<div className={'universal-nav-left'}>
<SearchBar />
</div>
<div className='universal-nav-middle'>
<Link className='universal-nav-logo' to='/'>
<NavLogo />
</Link>
</div>
<div className='universal-nav-right main-nav'>
<NavLinks />
</div>
</nav>
);
}
UniversalNav.displayName = 'UniversalNav';
export default UniversalNav;

View File

@ -1,24 +0,0 @@
#top-nav .menu-button {
color: white;
background-color: transparent;
margin: 0 20px 0 12px;
padding: 2px 14px;
border: 1px solid #fff;
}
#top-nav .menu-button-open {
background: white;
color: var(--theme-color);
}
@media (min-width: 735px) {
#top-nav .top-menu-button {
display: none;
}
}
@media (max-width: 420px) {
#top-nav .menu-button {
margin: 0 10px 0 4px;
}
}

View File

@ -1,66 +0,0 @@
#top-right-nav {
display: flex;
margin: 0;
list-style: none;
justify-content: space-between;
align-items: center;
}
#top-right-nav li {
margin: 0;
}
.top-right-nav-link {
max-height: var(--header-height);
color: #fff;
font-size: 18px;
padding: 8px 15px;
text-decoration: none;
}
.top-right-nav-link:hover,
.top-right-nav-link:focus,
.top-right-nav-link:active {
background-color: #fff;
color: var(--theme-color);
text-decoration: none;
}
.user-state-spinner {
height: var(--header-height);
padding: 0 12px;
}
.user-state-spinner > div {
animation-duration: 1.5s !important;
}
.top-nav-expanded {
background-color: var(--theme-color);
}
#top-right-nav .signup-btn,
#top-right-nav .settings-link {
margin: 0 15px;
}
#top-right-nav .user-avatar {
max-height: calc(var(--header-height) - 4px);
}
@media (max-width: 734px) {
#top-right-nav {
position: absolute;
top: var(--header-height);
flex-direction: column;
width: 100vw;
height: min-content;
min-height: 160px;
padding: 10px 0;
display: none;
}
#top-right-nav.top-nav-expanded {
display: flex;
}
}

View File

@ -0,0 +1,145 @@
.universal-nav {
z-index: 300;
display: flex;
justify-content: space-between;
align-items: flex-start;
/* overflow-y: hidden; */
height: 40px;
font-size: 18px;
font-family: 'Lato', sans-serif;
height: calc(2 * var(--header-height));
background: var(--theme-color);
position: absolute;
z-index: 1000;
color: var(--gray-00);
width: 100%;
padding: 0 15px;
top: 0;
}
.universal-nav a {
text-decoration: none;
}
.universal-nav-middle {
display: flex;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
margin-right: 10px;
letter-spacing: 0.4px;
white-space: nowrap;
-ms-overflow-scrolling: touch;
margin-right: 0;
}
.universal-nav-logo {
color: var(--gray-00);
font-size: 1.7rem;
line-height: 1em;
font-weight: bold;
letter-spacing: -0.5px;
display: flex;
flex-shrink: 0;
position: absolute;
left: 15px;
top: 0px;
}
.universal-nav-logo:hover {
text-decoration: none;
background-color: var(--theme-color);
}
.universal-nav-logo img {
display: block;
width: auto;
height: 25px;
margin-top: 7px;
}
.nav-list {
display: flex;
margin: 0 0 0 -12px;
padding: 0;
list-style: none;
}
.nav-list {
height: var(--header-height);
}
.nav-list li {
display: block;
margin: 0;
padding: 0;
}
.nav-list li a {
display: block;
margin: 0;
padding: 8px 15px;
color: var(--gray-00);
opacity: 1;
white-space: nowrap;
height: var(--header-height);
}
.nav-list li:hover {
background: var(--gray-00);
}
.nav-list li a:hover {
color: var(--theme-color);
text-decoration: none;
background: var(--gray-00);
}
.universal-nav-right {
flex-shrink: 0;
display: flex;
align-items: center;
height: 38px;
}
@media (max-width: 380px) {
.universal-nav {
padding: 0px 5px;
}
.universal-nav-logo {
left: 5px;
}
.nav-list li a {
padding: 8px 5px;
}
}
@media (min-width: 700px) {
.universal-nav-logo {
margin-left: auto;
margin-right: auto;
position: static;
left: auto;
top: auto;
}
.main-nav-group {
margin-left: auto;
}
.universal-nav-middle {
flex: 1 0 33%;
margin-right: 0;
margin-left: 0;
}
.universal-nav-left {
flex: 1 0 33%;
margin-left: 0px;
}
.universal-nav-right {
flex: 1 0 33%;
margin-left: auto;
}
.universal-nav {
height: var(--header-height);
}
}

View File

@ -4,59 +4,3 @@ header {
width: 100%; width: 100%;
z-index: 200; z-index: 200;
} }
#top-nav {
background: var(--theme-color);
height: var(--header-height);
margin-bottom: 0;
border-radius: 0;
border: none;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px;
}
#top-nav .home-link {
display: flex;
align-items: center;
}
#top-nav .home-link:hover {
background-color: var(--theme-color);
}
/* Navbar logo */
#top-nav .nav-logo {
max-height: 25px;
min-width: 35px;
margin: 0 5px;
}
.logoContainer {
margin-right: 10px;
}
@media (min-width: 735px) {
.logoContainer {
margin-right: 50px;
width: 25%;
}
}
@media (max-width: 734px) {
#top-nav {
padding: 0;
}
#top-nav .nav-logo {
margin: 0 0 0 10px;
}
}
@media (max-width: 420px) {
#top-nav .nav-logo {
margin: 0 0 0 5px;
}
}

View File

@ -1,38 +1,22 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import SearchBar from '../search/searchBar/SearchBar';
import NavigationMenu from './components/NavMenu'; import UniversalNav from './components/UniversalNav';
import NavLogo from './components/NavLogo';
import { Link } from '../helpers';
import './header.css'; import './header.css';
const propTypes = { export function Header() {
disableSettings: PropTypes.bool
};
function Header(props) {
const { disableSettings } = props;
return ( return (
<> <>
<Helmet> <Helmet>
<style>{':root{--header-height: 38px}'}</style> <style>{':root{--header-height: 38px}'}</style>
</Helmet> </Helmet>
<header> <header>
<nav id='top-nav'> <UniversalNav />
<Link className='home-link' to='/'>
<NavLogo />
</Link>
<SearchBar />
<NavigationMenu disableSettings={disableSettings} />
</nav>
</header> </header>
</> </>
); );
} }
Header.propTypes = propTypes; Header.displayName = 'Header';
export default Header; export default Header;

View File

@ -154,7 +154,7 @@ class DefaultLayout extends Component {
<style>{fontawesome.dom.css()}</style> <style>{fontawesome.dom.css()}</style>
</Helmet> </Helmet>
<WithInstantSearch> <WithInstantSearch>
<Header disableSettings={!isSignedIn} /> <Header />
<div className={`default-layout`}> <div className={`default-layout`}>
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} /> <OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
{hasMessage && flashMessage ? ( {hasMessage && flashMessage ? (

View File

@ -379,3 +379,7 @@ blockquote small,
blockquote .small { blockquote .small {
color: var(--gray-45); color: var(--gray-45);
} }
.alert {
border-radius: 0px;
}

View File

@ -18,45 +18,78 @@
.ais-SearchBox-input { .ais-SearchBox-input {
padding: 1px 10px; padding: 1px 10px;
font-size: 18px; font-size: 18px;
display: inline-block;
width: calc(100vw - 10px);
} }
.fcc_searchBar .ais-SearchBox-input, .fcc_searchBar .ais-SearchBox-input,
.fcc_searchBar .ais-Hits { .fcc_searchBar .ais-Hits {
z-index: 100;
background-color: var(--gray-75); background-color: var(--gray-75);
color: var(--gray-00); color: var(--gray-00);
} }
.fcc_searchBar .ais-SearchBox-form { .fcc_searchBar .ais-SearchBox-form {
margin-bottom: 0; margin-bottom: 0;
display: flex;
position: absolute;
top: var(--header-height);
right: 5px;
} }
.fcc_searchBar .ais-Hits { .fcc_searchBar .ais-Hits {
width: 100%; top: 71px;
left: 0; width: calc(100vw - 10px);
left: 5px;
} }
@media (min-width: 480px) { #fcc_instantsearch {
margin-top: 6px;
}
@media (min-width: 380px) {
.fcc_searchBar .ais-Hits { .fcc_searchBar .ais-Hits {
width: 90%; width: calc(100vw - 30px);
left: 5%; left: 15px;
}
.ais-SearchBox-input {
width: calc(100vw - 30px);
}
.fcc_searchBar .ais-SearchBox-form {
display: flex;
position: absolute;
top: var(--header-height);
right: 15px;
} }
} }
@media (min-width: 735px) { @media (min-width: 700px) {
.fcc_searchBar .ais-Hits { .ais-SearchBox-input {
width: 80%; width: 100%;
left: 10%; }
#fcc_instantsearch {
margin-top: 6px;
max-width: 500px;
} }
}
@media (min-width: 992px) {
.fcc_searchBar { .fcc_searchBar {
position: relative; position: relative;
} }
.fcc_searchBar .ais-Hits { .fcc_searchBar .ais-Hits {
width: calc(100% - 20px); top: auto;
width: calc(80vw - 20px);
left: 10px; left: 10px;
} }
.fcc_searchBar .ais-SearchBox-form {
display: flex;
position: static;
top: auto;
right: 15px;
}
}
@media (min-width: 1100px) {
.fcc_searchBar .ais-Hits {
width: calc(100% - 20px);
}
} }
/* hits */ /* hits */

View File

@ -52,7 +52,7 @@ function Welcome({ name }) {
<Link className='btn btn-lg btn-primary btn-block' to='/learn'> <Link className='btn btn-lg btn-primary btn-block' to='/learn'>
Build Projects and Earn Certifications Build Projects and Earn Certifications
</Link> </Link>
<Link className='btn btn-lg btn-primary btn-block' to='settings'> <Link className='btn btn-lg btn-primary btn-block' to='/settings'>
Update Your Developer Portfolio Update Your Developer Portfolio
</Link> </Link>
<Link <Link

View File

@ -1,17 +1,39 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { StripeProvider, Elements } from 'react-stripe-elements'; import { StripeProvider, Elements } from 'react-stripe-elements';
import { Grid, Row, Col, Button } from '@freecodecamp/react-bootstrap'; import { Grid, Row, Col, Button } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { stripePublicKey } from '../../config/env.json'; import { stripePublicKey, apiLocation } from '../../config/env.json';
import { Spacer, Loader } from '../components/helpers';
import Spacer from '../components/helpers/Spacer';
import DonateOther from '../components/Donation/components/DonateOther'; import DonateOther from '../components/Donation/components/DonateOther';
import DonateForm from '../components/Donation/components/DonateForm'; import DonateForm from '../components/Donation/components/DonateForm';
import DonateText from '../components/Donation/components/DonateText'; import DonateText from '../components/Donation/components/DonateText';
import PoweredByStripe from '../components/Donation/components/poweredByStripe'; import PoweredByStripe from '../components/Donation/components/poweredByStripe';
import { signInLoadingSelector, isSignedInSelector, hardGoTo } from '../redux';
class DonatePage extends Component { const mapStateToProps = createSelector(
signInLoadingSelector,
isSignedInSelector,
(showLoading, isSignedIn) => ({
showLoading,
isSignedIn
})
);
const mapDispatchToProps = dispatch => ({
navigate: location => dispatch(hardGoTo(location))
});
const propTypes = {
isSignedIn: PropTypes.bool.isRequired,
navigate: PropTypes.func.isRequired,
showLoading: PropTypes.bool.isRequired
};
export class DonatePage extends Component {
constructor(...props) { constructor(...props) {
super(...props); super(...props);
this.state = { this.state = {
@ -28,7 +50,7 @@ class DonatePage extends Component {
...state, ...state,
stripe: window.Stripe(stripePublicKey) stripe: window.Stripe(stripePublicKey)
})); }));
} else { } else if (document.querySelector('#stripe-js')) {
document document
.querySelector('#stripe-js') .querySelector('#stripe-js')
.addEventListener('load', this.handleStripeLoad); .addEventListener('load', this.handleStripeLoad);
@ -60,6 +82,16 @@ class DonatePage extends Component {
render() { render() {
const { showOtherOptions, stripe } = this.state; const { showOtherOptions, stripe } = this.state;
const { showLoading, isSignedIn, navigate } = this.props;
if (showLoading) {
return <Loader fullScreen={true} />;
}
if (!showLoading && !isSignedIn) {
return navigate(`${apiLocation}/signin`);
}
return ( return (
<Fragment> <Fragment>
<Helmet title='Support our nonprofit | freeCodeCamp.org' /> <Helmet title='Support our nonprofit | freeCodeCamp.org' />
@ -97,5 +129,9 @@ class DonatePage extends Component {
} }
DonatePage.displayName = 'DonatePage'; DonatePage.displayName = 'DonatePage';
DonatePage.propTypes = propTypes;
export default DonatePage; export default connect(
mapStateToProps,
mapDispatchToProps
)(DonatePage);

View File

@ -0,0 +1,25 @@
/* global jest, expect */
import React from 'react';
import 'jest-dom/extend-expect';
import ShallowRenderer from 'react-test-renderer/shallow';
import { apiLocation } from '../../config/env.json';
import { DonatePage } from './donate';
describe('<ShowSettings />', () => {
it('redirects to signin page when user not logged in', () => {
const shallow = new ShallowRenderer();
shallow.render(<DonatePage {...loggedOutProps} />);
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(`${apiLocation}/signin`);
expect(true).toBeTruthy();
});
});
const navigate = jest.fn();
const loggedOutProps = {
createFlashMessage: () => {},
isSignedIn: false,
showLoading: false,
navigate: navigate
};

View File

@ -7,6 +7,10 @@
top: 38px; top: 38px;
} }
.learn-page-wrapper .signup-btn {
width: 100%;
}
@media screen and (max-width: 630px) { @media screen and (max-width: 630px) {
.learn-page-wrapper { .learn-page-wrapper {
padding: 0 40px; padding: 0 40px;

View File

@ -1,8 +1,19 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createSelector } from 'reselect';
import { graphql } from 'gatsby'; import { graphql } from 'gatsby';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Row, Col } from '@freecodecamp/react-bootstrap';
import { userFetchStateSelector, isSignedInSelector } from '../redux';
import LearnLayout from '../components/layouts/Learn';
import Login from '../components/Header/components/Login';
import { Link, Spacer, Loader } from '../components/helpers';
import Map from '../components/Map';
import './learn.css';
import { import {
ChallengeNode, ChallengeNode,
@ -10,23 +21,48 @@ import {
AllMarkdownRemark AllMarkdownRemark
} from '../redux/propTypes'; } from '../redux/propTypes';
import LearnLayout from '../components/layouts/Learn'; const mapStateToProps = createSelector(
import { Link, Spacer } from '../components/helpers'; userFetchStateSelector,
import Map from '../components/Map'; isSignedInSelector,
(fetchState, isSignedIn) => ({
import './learn.css'; fetchState,
isSignedIn
const mapStateToProps = () => ({}); })
);
const propTypes = { const propTypes = {
data: PropTypes.shape({ data: PropTypes.shape({
challengeNode: ChallengeNode, challengeNode: ChallengeNode,
allChallengeNode: AllChallengeNode, allChallengeNode: AllChallengeNode,
allMarkdownRemark: AllMarkdownRemark allMarkdownRemark: AllMarkdownRemark
}) }),
fetchState: PropTypes.shape({
pending: PropTypes.bool,
complete: PropTypes.bool,
errored: PropTypes.bool
}),
isSignedIn: PropTypes.bool
};
const BigCallToAction = isSignedIn => {
if (!isSignedIn) {
return (
<>
<Spacer size={2} />
<Row>
<Col sm={8} smOffset={2} xs={12}>
<Login className={'text-center'}>Sign in to save progress.</Login>
</Col>
</Row>
</>
);
}
return '';
}; };
const IndexPage = ({ const IndexPage = ({
fetchState: { pending, complete },
isSignedIn,
data: { data: {
challengeNode: { challengeNode: {
fields: { slug } fields: { slug }
@ -34,37 +70,46 @@ const IndexPage = ({
allChallengeNode: { edges }, allChallengeNode: { edges },
allMarkdownRemark: { edges: mdEdges } allMarkdownRemark: { edges: mdEdges }
} }
}) => ( }) => {
<LearnLayout> if (pending && !complete) {
<div className='learn-page-wrapper'> return <Loader fullScreen={true} />;
<Helmet title='Learn | freeCodeCamp.org' /> }
<Spacer size={2} />
<h1 className='text-center'>Welcome to the freeCodeCamp curriculum</h1> return (
<p> <LearnLayout>
We have thousands of coding lessons to help you improve your skills. <div className='learn-page-wrapper'>
</p> <Helmet title='Learn | freeCodeCamp.org' />
<p>You can earn each certification by completing its 5 final projects.</p> {BigCallToAction(isSignedIn)}
<p> <Spacer size={2} />
And yes - all of this is 100% free, thanks to the thousands of campers <h1 className='text-center'>Welcome to the freeCodeCamp curriculum</h1>
who{' '} <p>
<Link external={true} to='/donate'> We have thousands of coding lessons to help you improve your skills.
donate </p>
</Link>{' '} <p>
to our nonprofit. You can earn each certification by completing its 5 final projects.
</p> </p>
<p> <p>
If you are new to coding, we recommend you{' '} And yes - all of this is 100% free, thanks to the thousands of campers
<Link to={slug}>start at the beginning</Link>. who{' '}
</p> <Link external={true} to='/donate'>
<Map donate
introNodes={mdEdges.map(({ node }) => node)} </Link>{' '}
nodes={edges to our nonprofit.
.map(({ node }) => node) </p>
.filter(({ isPrivate }) => !isPrivate)} <p>
/> If you are new to coding, we recommend you{' '}
</div> <Link to={slug}>start at the beginning</Link>.
</LearnLayout> </p>
); <Map
introNodes={mdEdges.map(({ node }) => node)}
nodes={edges
.map(({ node }) => node)
.filter(({ isPrivate }) => !isPrivate)}
/>
</div>
</LearnLayout>
);
};
IndexPage.displayName = 'IndexPage'; IndexPage.displayName = 'IndexPage';
IndexPage.propTypes = propTypes; IndexPage.propTypes = propTypes;

View File

@ -0,0 +1,72 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from '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
} from '../redux';
import Loader from '../components/helpers/Loader';
const mapStateToProps = createSelector(
signInLoadingSelector,
userSelector,
isSignedInSelector,
(showLoading, user, isSignedIn) => ({
showLoading,
user,
isSignedIn
})
);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
hardGoTo,
navigate: location => dispatch(hardGoTo(location))
},
dispatch
);
const propTypes = {
hardGoTo: PropTypes.func.isRequired,
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) {
return navigate(`${apiLocation}/signin`);
}
const RedirecUser = createRedirect('/' + username);
return <RedirecUser />;
}
ProfilePage.displayName = 'profilePage';
ProfilePage.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProfilePage);