From e653235d94dbd06a140d18371d7fbd130ed6f9ad Mon Sep 17 00:00:00 2001 From: Ahmad Abdolsaheb Date: Thu, 19 Sep 2019 19:45:01 +0300 Subject: [PATCH] feat: universal navbar (#36744) * feat: add universal nav * fix: add portfolio redirect --- client/src/__mocks__/gatsby.js | 1 + client/src/client-only-routes/ShowSettings.js | 12 +- .../client-only-routes/ShowSettings.test.js | 37 +++++ client/src/components/Header/Header.test.js | 41 +++++ .../Header/components/MenuButton.js | 29 ---- .../components/Header/components/MenuLinks.js | 40 ----- .../components/Header/components/NavLinks.js | 20 +++ .../components/Header/components/NavLogo.js | 6 +- .../components/Header/components/NavMenu.js | 65 -------- .../Header/components/UniversalNav.js | 27 ++++ .../Header/components/menuButton.css | 24 --- .../Header/components/menuLinks.css | 66 -------- .../Header/components/universalNav.css | 145 ++++++++++++++++++ client/src/components/Header/header.css | 56 ------- client/src/components/Header/index.js | 24 +-- client/src/components/layouts/Default.js | 2 +- client/src/components/layouts/global.css | 4 + .../components/search/searchBar/searchbar.css | 59 +++++-- client/src/components/welcome/index.js | 2 +- client/src/pages/donate.js | 48 +++++- client/src/pages/donate.test.js | 25 +++ client/src/pages/learn.css | 4 + client/src/pages/learn.js | 123 ++++++++++----- client/src/pages/portfolio.js | 72 +++++++++ 24 files changed, 562 insertions(+), 370 deletions(-) create mode 100644 client/src/client-only-routes/ShowSettings.test.js create mode 100644 client/src/components/Header/Header.test.js delete mode 100644 client/src/components/Header/components/MenuButton.js delete mode 100644 client/src/components/Header/components/MenuLinks.js create mode 100644 client/src/components/Header/components/NavLinks.js delete mode 100644 client/src/components/Header/components/NavMenu.js create mode 100644 client/src/components/Header/components/UniversalNav.js delete mode 100644 client/src/components/Header/components/menuButton.css delete mode 100644 client/src/components/Header/components/menuLinks.css create mode 100644 client/src/components/Header/components/universalNav.css create mode 100644 client/src/pages/donate.test.js create mode 100644 client/src/pages/portfolio.js diff --git a/client/src/__mocks__/gatsby.js b/client/src/__mocks__/gatsby.js index 006a72c0ee..53952b33fc 100644 --- a/client/src/__mocks__/gatsby.js +++ b/client/src/__mocks__/gatsby.js @@ -6,6 +6,7 @@ const gatsby = jest.requireActual('gatsby'); module.exports = { ...gatsby, + navigate: jest.fn(), graphql: jest.fn(), Link: jest.fn().mockImplementation( // these props are invalid for an `a` tag diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js index 6029b13e69..b413101a1b 100644 --- a/client/src/client-only-routes/ShowSettings.js +++ b/client/src/client-only-routes/ShowSettings.js @@ -27,13 +27,13 @@ import Portfolio from '../components/settings/Portfolio'; import Honesty from '../components/settings/Honesty'; import Certification from '../components/settings/Certification'; import DangerZone from '../components/settings/DangerZone'; -import RedirectHome from '../components/RedirectHome'; const propTypes = { createFlashMessage: PropTypes.func.isRequired, hardGoTo: PropTypes.func.isRequired, - isSignedIn: PropTypes.bool, - showLoading: PropTypes.bool, + isSignedIn: PropTypes.bool.isRequired, + navigate: PropTypes.func.isRequired, + showLoading: PropTypes.bool.isRequired, submitNewAbout: PropTypes.func.isRequired, toggleNightMode: PropTypes.func.isRequired, updateInternetSettings: PropTypes.func.isRequired, @@ -105,6 +105,7 @@ const mapDispatchToProps = dispatch => { createFlashMessage, hardGoTo, + navigate: location => dispatch(hardGoTo(location)), submitNewAbout, toggleNightMode: theme => updateUserFlag({ theme }), updateInternetSettings: updateUserFlag, @@ -121,7 +122,7 @@ const createHandleSignoutClick = hardGoTo => e => { return hardGoTo(`${apiLocation}/signout`); }; -function ShowSettings(props) { +export function ShowSettings(props) { const { createFlashMessage, hardGoTo, @@ -157,6 +158,7 @@ function ShowSettings(props) { website, portfolio }, + navigate, showLoading, updateQuincyEmail, updateInternetSettings, @@ -170,7 +172,7 @@ function ShowSettings(props) { } if (!showLoading && !isSignedIn) { - return ; + return navigate(`${apiLocation}/signin`); } return ( diff --git a/client/src/client-only-routes/ShowSettings.test.js b/client/src/client-only-routes/ShowSettings.test.js new file mode 100644 index 0000000000..30be96c765 --- /dev/null +++ b/client/src/client-only-routes/ShowSettings.test.js @@ -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('', () => { + it('redirects to signin page when user not logged in', () => { + const shallow = new ShallowRenderer(); + shallow.render(); + 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() +}; diff --git a/client/src/components/Header/Header.test.js b/client/src/components/Header/Header.test.js new file mode 100644 index 0000000000..67ee76b6ee --- /dev/null +++ b/client/src/components/Header/Header.test.js @@ -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('
', () => { + it('renders to the DOM', () => { + const shallow = new ShallowRenderer(); + shallow.render(
); + const result = shallow.getRenderOutput(); + expect(result).toBeTruthy(); + }); +}); + +describe('', () => { + const root = TestRenderer.create().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(); + }); +}); diff --git a/client/src/components/Header/components/MenuButton.js b/client/src/components/Header/components/MenuButton.js deleted file mode 100644 index d9d1830b28..0000000000 --- a/client/src/components/Header/components/MenuButton.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import './menuButton.css'; - -const MenuButton = React.forwardRef((props, ref) => ( - -)); - -MenuButton.displayName = 'MenuButton'; -MenuButton.propTypes = { - className: PropTypes.string, - displayMenu: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired -}; - -export default MenuButton; diff --git a/client/src/components/Header/components/MenuLinks.js b/client/src/components/Header/components/MenuLinks.js deleted file mode 100644 index 602cca4e46..0000000000 --- a/client/src/components/Header/components/MenuLinks.js +++ /dev/null @@ -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 ( -
    -
  • - - Learn - -
  • -
  • - - Forum - -
  • -
  • - - News - -
  • -
  • - -
  • -
- ); -} - -MenuLinks.displayName = 'MenuLinks'; -MenuLinks.propTypes = { - className: PropTypes.string, - disableSettings: PropTypes.bool -}; - -export default MenuLinks; diff --git a/client/src/components/Header/components/NavLinks.js b/client/src/components/Header/components/NavLinks.js new file mode 100644 index 0000000000..80184615b1 --- /dev/null +++ b/client/src/components/Header/components/NavLinks.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { Link } from '../../helpers'; + +export function NavLinks() { + return ( +
+
    +
  • + Light +
  • +
  • + Portfolio +
  • +
+
+ ); +} + +NavLinks.displayName = 'NavLinks'; +export default NavLinks; diff --git a/client/src/components/Header/components/NavLogo.js b/client/src/components/Header/components/NavLogo.js index 9773a324e5..87f7e6cdc3 100644 --- a/client/src/components/Header/components/NavLogo.js +++ b/client/src/components/Header/components/NavLogo.js @@ -1,22 +1,18 @@ import React from 'react'; const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg'; -const fCCglyph = 'https://s3.amazonaws.com/freecodecamp/FFCFire.png'; function NavLogo() { return ( - - learn to code at freeCodeCamp logo ); } NavLogo.displayName = 'NavLogo'; - export default NavLogo; diff --git a/client/src/components/Header/components/NavMenu.js b/client/src/components/Header/components/NavMenu.js deleted file mode 100644 index 9f0377fa23..0000000000 --- a/client/src/components/Header/components/NavMenu.js +++ /dev/null @@ -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 ( - <> - - - - ); - } -} - -NavigationMenu.displayName = 'NavigationMenu'; -NavigationMenu.propTypes = { - disableSettings: PropTypes.bool -}; - -export default NavigationMenu; diff --git a/client/src/components/Header/components/UniversalNav.js b/client/src/components/Header/components/UniversalNav.js new file mode 100644 index 0000000000..ce6230d4c0 --- /dev/null +++ b/client/src/components/Header/components/UniversalNav.js @@ -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 ( + + ); +} + +UniversalNav.displayName = 'UniversalNav'; +export default UniversalNav; diff --git a/client/src/components/Header/components/menuButton.css b/client/src/components/Header/components/menuButton.css deleted file mode 100644 index 446c88b541..0000000000 --- a/client/src/components/Header/components/menuButton.css +++ /dev/null @@ -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; - } -} diff --git a/client/src/components/Header/components/menuLinks.css b/client/src/components/Header/components/menuLinks.css deleted file mode 100644 index abf5842670..0000000000 --- a/client/src/components/Header/components/menuLinks.css +++ /dev/null @@ -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; - } -} diff --git a/client/src/components/Header/components/universalNav.css b/client/src/components/Header/components/universalNav.css new file mode 100644 index 0000000000..8172b3f8a8 --- /dev/null +++ b/client/src/components/Header/components/universalNav.css @@ -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); + } +} diff --git a/client/src/components/Header/header.css b/client/src/components/Header/header.css index 00b9ec2455..29ed47851b 100644 --- a/client/src/components/Header/header.css +++ b/client/src/components/Header/header.css @@ -4,59 +4,3 @@ header { width: 100%; 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; - } -} diff --git a/client/src/components/Header/index.js b/client/src/components/Header/index.js index 9b13cda122..94fb3f8604 100644 --- a/client/src/components/Header/index.js +++ b/client/src/components/Header/index.js @@ -1,38 +1,22 @@ import React from 'react'; -import PropTypes from 'prop-types'; import Helmet from 'react-helmet'; -import SearchBar from '../search/searchBar/SearchBar'; -import NavigationMenu from './components/NavMenu'; -import NavLogo from './components/NavLogo'; -import { Link } from '../helpers'; +import UniversalNav from './components/UniversalNav'; import './header.css'; -const propTypes = { - disableSettings: PropTypes.bool -}; - -function Header(props) { - const { disableSettings } = props; +export function Header() { return ( <>
- +
); } -Header.propTypes = propTypes; - +Header.displayName = 'Header'; export default Header; diff --git a/client/src/components/layouts/Default.js b/client/src/components/layouts/Default.js index d490bb6faa..bb987bfa74 100644 --- a/client/src/components/layouts/Default.js +++ b/client/src/components/layouts/Default.js @@ -154,7 +154,7 @@ class DefaultLayout extends Component { -
+
{hasMessage && flashMessage ? ( diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css index 6546c72a5f..b9847dbb0e 100644 --- a/client/src/components/layouts/global.css +++ b/client/src/components/layouts/global.css @@ -379,3 +379,7 @@ blockquote small, blockquote .small { color: var(--gray-45); } + +.alert { + border-radius: 0px; +} diff --git a/client/src/components/search/searchBar/searchbar.css b/client/src/components/search/searchBar/searchbar.css index 434323dc90..c9f1be2b62 100644 --- a/client/src/components/search/searchBar/searchbar.css +++ b/client/src/components/search/searchBar/searchbar.css @@ -18,45 +18,78 @@ .ais-SearchBox-input { padding: 1px 10px; font-size: 18px; + display: inline-block; + width: calc(100vw - 10px); } .fcc_searchBar .ais-SearchBox-input, .fcc_searchBar .ais-Hits { + z-index: 100; background-color: var(--gray-75); color: var(--gray-00); } .fcc_searchBar .ais-SearchBox-form { margin-bottom: 0; + display: flex; + position: absolute; + top: var(--header-height); + right: 5px; } .fcc_searchBar .ais-Hits { - width: 100%; - left: 0; + top: 71px; + width: calc(100vw - 10px); + left: 5px; } -@media (min-width: 480px) { +#fcc_instantsearch { + margin-top: 6px; +} + +@media (min-width: 380px) { .fcc_searchBar .ais-Hits { - width: 90%; - left: 5%; + width: calc(100vw - 30px); + 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) { - .fcc_searchBar .ais-Hits { - width: 80%; - left: 10%; +@media (min-width: 700px) { + .ais-SearchBox-input { + width: 100%; + } + #fcc_instantsearch { + margin-top: 6px; + max-width: 500px; } -} - -@media (min-width: 992px) { .fcc_searchBar { position: relative; } .fcc_searchBar .ais-Hits { - width: calc(100% - 20px); + top: auto; + width: calc(80vw - 20px); 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 */ diff --git a/client/src/components/welcome/index.js b/client/src/components/welcome/index.js index 99621ef51a..98f57aef24 100644 --- a/client/src/components/welcome/index.js +++ b/client/src/components/welcome/index.js @@ -52,7 +52,7 @@ function Welcome({ name }) { Build Projects and Earn Certifications - + Update Your Developer Portfolio ({ + 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) { super(...props); this.state = { @@ -28,7 +50,7 @@ class DonatePage extends Component { ...state, stripe: window.Stripe(stripePublicKey) })); - } else { + } else if (document.querySelector('#stripe-js')) { document .querySelector('#stripe-js') .addEventListener('load', this.handleStripeLoad); @@ -60,6 +82,16 @@ class DonatePage extends Component { render() { const { showOtherOptions, stripe } = this.state; + const { showLoading, isSignedIn, navigate } = this.props; + + if (showLoading) { + return ; + } + + if (!showLoading && !isSignedIn) { + return navigate(`${apiLocation}/signin`); + } + return ( @@ -97,5 +129,9 @@ class DonatePage extends Component { } DonatePage.displayName = 'DonatePage'; +DonatePage.propTypes = propTypes; -export default DonatePage; +export default connect( + mapStateToProps, + mapDispatchToProps +)(DonatePage); diff --git a/client/src/pages/donate.test.js b/client/src/pages/donate.test.js new file mode 100644 index 0000000000..fd4e324207 --- /dev/null +++ b/client/src/pages/donate.test.js @@ -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('', () => { + it('redirects to signin page when user not logged in', () => { + const shallow = new ShallowRenderer(); + shallow.render(); + 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 +}; diff --git a/client/src/pages/learn.css b/client/src/pages/learn.css index e635cdda0e..d0224c57aa 100644 --- a/client/src/pages/learn.css +++ b/client/src/pages/learn.css @@ -7,6 +7,10 @@ top: 38px; } +.learn-page-wrapper .signup-btn { + width: 100%; +} + @media screen and (max-width: 630px) { .learn-page-wrapper { padding: 0 40px; diff --git a/client/src/pages/learn.js b/client/src/pages/learn.js index 9c08bbafcc..e1839bf7b6 100644 --- a/client/src/pages/learn.js +++ b/client/src/pages/learn.js @@ -1,8 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; import { graphql } from 'gatsby'; import Helmet from 'react-helmet'; 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 { ChallengeNode, @@ -10,23 +21,48 @@ import { AllMarkdownRemark } from '../redux/propTypes'; -import LearnLayout from '../components/layouts/Learn'; -import { Link, Spacer } from '../components/helpers'; -import Map from '../components/Map'; - -import './learn.css'; - -const mapStateToProps = () => ({}); +const mapStateToProps = createSelector( + userFetchStateSelector, + isSignedInSelector, + (fetchState, isSignedIn) => ({ + fetchState, + isSignedIn + }) +); const propTypes = { data: PropTypes.shape({ challengeNode: ChallengeNode, allChallengeNode: AllChallengeNode, allMarkdownRemark: AllMarkdownRemark - }) + }), + fetchState: PropTypes.shape({ + pending: PropTypes.bool, + complete: PropTypes.bool, + errored: PropTypes.bool + }), + isSignedIn: PropTypes.bool +}; + +const BigCallToAction = isSignedIn => { + if (!isSignedIn) { + return ( + <> + + + + Sign in to save progress. + + + + ); + } + return ''; }; const IndexPage = ({ + fetchState: { pending, complete }, + isSignedIn, data: { challengeNode: { fields: { slug } @@ -34,37 +70,46 @@ const IndexPage = ({ allChallengeNode: { edges }, allMarkdownRemark: { edges: mdEdges } } -}) => ( - -
- - -

Welcome to the freeCodeCamp curriculum

-

- We have thousands of coding lessons to help you improve your skills. -

-

You can earn each certification by completing its 5 final projects.

-

- And yes - all of this is 100% free, thanks to the thousands of campers - who{' '} - - donate - {' '} - to our nonprofit. -

-

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

- node)} - nodes={edges - .map(({ node }) => node) - .filter(({ isPrivate }) => !isPrivate)} - /> -
-
-); +}) => { + if (pending && !complete) { + return ; + } + + return ( + +
+ + {BigCallToAction(isSignedIn)} + +

Welcome to the freeCodeCamp curriculum

+

+ We have thousands of coding lessons to help you improve your skills. +

+

+ You can earn each certification by completing its 5 final projects. +

+

+ And yes - all of this is 100% free, thanks to the thousands of campers + who{' '} + + donate + {' '} + to our nonprofit. +

+

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

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