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 (
-
-
);
}
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.
-
-
-
-);
+}) => {
+ 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.
+
+
+
+ );
+};
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);