feat: universal navbar (#36744)
* feat: add universal nav * fix: add portfolio redirect
This commit is contained in:
committed by
mrugesh
parent
5d946f3d77
commit
e653235d94
@ -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
|
||||||
|
@ -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 (
|
||||||
|
37
client/src/client-only-routes/ShowSettings.test.js
Normal file
37
client/src/client-only-routes/ShowSettings.test.js
Normal 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()
|
||||||
|
};
|
41
client/src/components/Header/Header.test.js
Normal file
41
client/src/components/Header/Header.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
|
@ -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;
|
|
20
client/src/components/Header/components/NavLinks.js
Normal file
20
client/src/components/Header/components/NavLinks.js
Normal 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;
|
@ -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;
|
||||||
|
@ -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;
|
|
27
client/src/components/Header/components/UniversalNav.js
Normal file
27
client/src/components/Header/components/UniversalNav.js
Normal 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;
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
145
client/src/components/Header/components/universalNav.css
Normal file
145
client/src/components/Header/components/universalNav.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
|
@ -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 ? (
|
||||||
|
@ -379,3 +379,7 @@ blockquote small,
|
|||||||
blockquote .small {
|
blockquote .small {
|
||||||
color: var(--gray-45);
|
color: var(--gray-45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
@ -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 */
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
25
client/src/pages/donate.test.js
Normal file
25
client/src/pages/donate.test.js
Normal 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
|
||||||
|
};
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
72
client/src/pages/portfolio.js
Normal file
72
client/src/pages/portfolio.js
Normal 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);
|
Reference in New Issue
Block a user