feat: render nav conditionally

This commit is contained in:
Ahmad Abdolsaheb
2020-09-03 14:21:16 +03:00
committed by Mrugesh Mohapatra
parent 422bacd15d
commit 1a66eac990
29 changed files with 640 additions and 180 deletions

View File

@ -21295,15 +21295,6 @@
"camelcase": "^5.0.0"
}
},
"react-identicons": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/react-identicons/-/react-identicons-1.2.4.tgz",
"integrity": "sha512-my4CFMlO88kWjhX/y5qiGjQE9KgVLLeUKEM2wilUko6UzUTmzHJvl0rjkcftG9bMq3WLkpJkB2qwFMRllS+NmQ==",
"requires": {
"react": "^16.13.0",
"react-dom": "^16.13.0"
}
},
"react-instantsearch-core": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.7.0.tgz",

View File

@ -56,7 +56,6 @@
"react-ga": "^2.7.0",
"react-helmet": "^5.2.1",
"react-hotkeys": "^2.0.0",
"react-identicons": "^1.1.7",
"react-instantsearch-dom": "^6.7.0",
"react-lazy-load": "^3.1.13",
"react-monaco-editor": "^0.36.0",

View File

@ -0,0 +1,91 @@
/* eslint-disable max-len */
import React from 'react';
function DefaultAvatar(props) {
return (
<svg
className='default-avatar'
height='500px'
version='1.1'
viewBox='0 0 500 500'
width='500px'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
{...props}
>
<title>default avatar</title>
<desc>an avatar conding with a laptop</desc>
<g fill='none' fillRule='evenodd'>
<g id='g'>
<rect fill='#D0D0D5' height='500' width='500' />
<path
d='m251.34 49c23.859 58.47 34.222 90.121 31.088 94.954-4.701 7.2493-15.381 32.924 0 50.968s77.487 6.439 92.029 23.897c14.542 17.458 12.196 68.184 14.542 135.56-22.154 0.54208-68.154 1.0837-138 1.6248l0.062-56h-0.124l0.062 56c-69.846-0.54109-115.85-1.0827-138-1.6248 2.3463-67.372 0-118.1 14.542-135.56 14.542-17.458 76.649-5.852 92.029-23.897s4.701-43.719 0-50.968c-3.134-4.8329 7.2285-36.484 31.088-94.954l0.13247 120h0.415z'
fill='#242440'
/>
<path
d='m250.13 185c47.577 0 66.872-66.034 66.872-90.434 0-42.286-29.773-76.566-66.5-76.566s-66.5 34.28-66.5 76.566c0 24.7 18.552 90.434 66.128 90.434z'
fill='#242440'
id='c'
stroke='#D0D0D5'
strokeWidth='17'
/>
<path
d='m77.011 459c-19.341-119.95-29.011-183.79-29.011-191.53 0-11.605 6.2167-16.473 17.298-16.473h370.4c11.082 0 17.298 4.8681 17.298 16.473 0 7.7366-9.6704 71.579-29.011 191.53z'
fill='#5F5F8C'
stroke='#D0D0D5'
strokeWidth='16'
/>
<rect
fill='#5F5F8C'
height='23'
stroke='#D0D0D5'
strokeWidth='6'
width='339'
x='81'
y='459'
/>
<g fillRule='nonzero' transform='translate(162 283)'>
<ellipse cx='88.5' cy='79' fill='#0A0A23' rx='88.5' ry='79' />
<g transform='translate(20 40)'>
<g id='Group' transform='translate(42.462 4)'>
<g fill='#fff' id='e'>
<path
d='m38.312 39.042c-3.9186-0.91476 12.174-18.263-16.43-39.034 0 0 3.7555 10.879-15.169 35.157-18.933 24.27 8.418 38.725 8.418 38.725s-12.834-6.24 2.0846-28.459c2.6734-4.0329 6.1663-7.6847 10.507-15.899 0 0 3.839 4.9441 1.834 15.671-2.9996 16.208 13.005 11.569 13.256 11.794 5.5895 6.0077-4.6307 16.564-5.2513 16.894-0.62061 0.32307 29.185-16.36 8.0083-41.469-1.4521 1.325-3.3338 7.5359-7.2564 6.6212z'
id='i'
/>
</g>
<g fill='#000' fillOpacity='0' stroke='#000' strokeOpacity='0'>
<path d='m38.312 39.042c-3.9186-0.91476 12.174-18.263-16.43-39.034 0 0 3.7555 10.879-15.169 35.157-18.933 24.27 8.418 38.725 8.418 38.725s-12.834-6.24 2.0846-28.459c2.6734-4.0329 6.1663-7.6847 10.507-15.899 0 0 3.839 4.9441 1.834 15.671-2.9996 16.208 13.005 11.569 13.256 11.794 5.5895 6.0077-4.6307 16.564-5.2513 16.894-0.62061 0.32307 29.185-16.36 8.0083-41.469-1.4521 1.325-3.3338 7.5359-7.2564 6.6212z' />
</g>
</g>
<g id='b' transform='translate(110.13)'>
<g fill='#fff' id='d'>
<path
d='m0.96996 0.62339c-0.47786 0.41439-0.95166 1.0162-0.95166 1.6215-0.0040664 1.045 1.3846 2.4611 3.9577 4.7889 10.713 9.1022 16.104 20.251 16.068 33.692-0.040843 14.875-5.7099 26.82-16.729 36.077-2.3158 1.8305-3.2674 3.2647-3.2715 4.4935 0 0.60537 0.4697 1.2324 0.94347 1.8341 0.44519 0.4216 1.3927 0.83962 2.0748 0.83962 2.5486 0.0071777 6.1183-2.6521 10.774-7.8266 9.0712-9.8085 13.172-20.64 13.401-35.4 0.21238-14.77-5.0319-24.784-15.308-35.126-3.6963-3.6935-6.7759-5.6141-8.8834-5.6177-0.68208 0-1.3927 0.20539-2.0748 0.62339z'
id='a'
/>
</g>
<g fill='#000' fillOpacity='0' stroke='#000' strokeOpacity='0'>
<path d='m0.96996 0.62339c-0.47786 0.41439-0.95166 1.0162-0.95166 1.6215-0.0040664 1.045 1.3846 2.4611 3.9577 4.7889 10.713 9.1022 16.104 20.251 16.068 33.692-0.040843 14.875-5.7099 26.82-16.729 36.077-2.3158 1.8305-3.2674 3.2647-3.2715 4.4935 0 0.60537 0.4697 1.2324 0.94347 1.8341 0.44519 0.4216 1.3927 0.83962 2.0748 0.83962 2.5486 0.0071777 6.1183-2.6521 10.774-7.8266 9.0712-9.8085 13.172-20.64 13.401-35.4 0.21238-14.77-5.0319-24.784-15.308-35.126-3.6963-3.6935-6.7759-5.6141-8.8834-5.6177-0.68208 0-1.3927 0.20539-2.0748 0.62339z' />
</g>
</g>
<g fill='#fff' id='h'>
<path
d='m26.367 0.6342c0.47409 0.41439 0.9482 1.0162 0.9482 1.6215 0.004069 1.045-1.3855 2.4611-3.9603 4.7889-10.72 9.1022-16.111 20.251-16.078 33.692 0.04087 14.875 5.7136 26.82 16.74 36.077 2.3173 1.8305 3.2696 3.2647 3.2737 4.4935 0 0.60537-0.47001 1.2324-0.9441 1.8341-0.44548 0.4216-1.3896 0.83962-2.0762 0.83962-2.5503 0.0071777-6.1223-2.6521-10.782-7.8266-9.0773-9.8085-13.181-20.64-13.409-35.4-0.21252-14.77 5.0352-24.784 15.318-35.126 3.6987-3.6935 6.7844-5.6141 8.8892-5.6177 0.68253 0 1.3937 0.20539 2.0803 0.62339z'
id='f'
/>
</g>
<g fill='#000' fillOpacity='0' stroke='#000' strokeOpacity='0'>
<path d='m26.367 0.6342c0.47409 0.41439 0.9482 1.0162 0.9482 1.6215 0.004069 1.045-1.3855 2.4611-3.9603 4.7889-10.72 9.1022-16.111 20.251-16.078 33.692 0.04087 14.875 5.7136 26.82 16.74 36.077 2.3173 1.8305 3.2696 3.2647 3.2737 4.4935 0 0.60537-0.47001 1.2324-0.9441 1.8341-0.44548 0.4216-1.3896 0.83962-2.0762 0.83962-2.5503 0.0071777-6.1223-2.6521-10.782-7.8266-9.0773-9.8085-13.181-20.64-13.409-35.4-0.21252-14.77 5.0352-24.784 15.318-35.126 3.6987-3.6935 6.7844-5.6141 8.8892-5.6177 0.68253 0 1.3937 0.20539 2.0803 0.62339z' />
</g>
</g>
</g>
</g>
</g>
</svg>
);
}
DefaultAvatar.displayName = 'DefaultAvatar';
export default DefaultAvatar;

View File

@ -15,7 +15,7 @@ import {
import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
import { createFlashMessage } from '../components/Flash/redux';
import { FullWidthRow, Link, Loader, Spacer } from '../components/helpers';
import { FullWidthRow, Loader, Spacer } from '../components/helpers';
import About from '../components/settings/About';
import Privacy from '../components/settings/Privacy';
import Email from '../components/settings/Email';
@ -177,13 +177,7 @@ export function ShowSettings(props) {
<Grid>
<main>
<Spacer size={2} />
<FullWidthRow className='button-group'>
<Link
className='btn-invert btn btn-lg btn-primary btn-block'
to={`/${username}`}
>
Show me my public portfolio
</Link>
<FullWidthRow>
<Button
block={true}
bsSize='lg'

View File

@ -1,13 +1,19 @@
/* global expect */
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import TestRenderer from 'react-test-renderer';
import { UniversalNav } from './components/UniversalNav';
import NavLinks from './components/NavLinks';
import renderer from 'react-test-renderer';
import { forumLocation } from '../../../config/env.json';
import { UniversalNav } from './components/UniversalNav';
import { AuthOrProfile } from './components/NavLinks';
describe('<UniversalNav />', () => {
const UniversalNavProps = {
displayMenu: false,
menuButtonRef: {},
searchBarRef: {},
toggleDisplayMenu: function() {},
pathName: '/'
};
it('renders to the DOM', () => {
const shallow = new ShallowRenderer();
shallow.render(<UniversalNav {...UniversalNavProps} />);
@ -15,34 +21,138 @@ describe('<UniversalNav />', () => {
expect(result).toBeTruthy();
});
});
describe('<NavLinks />', () => {
const root = TestRenderer.create(<NavLinks />).root;
const aTags = root.findAllByType('a');
it('shows Curriculum and Sign In buttons on landing page', () => {
const landingPageProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png'
},
pending: false,
pathName: '/'
};
const shallow = new ShallowRenderer();
shallow.render(<AuthOrProfile {...landingPageProps} />);
const result = shallow.getRenderOutput();
// expect(result.props.children).toEqual('Sign In');
// reduces the aTags to href links
const links = aTags.reduce((acc, item) => {
acc.push(item._fiber.pendingProps.href);
return acc;
}, []);
expect(deepChildrenProp(result, 0).children === 'Curriculum').toBeTruthy();
const expectedLinks = ['/learn', '/news', forumLocation];
it('renders to the DOM', () => {
expect(root).toBeTruthy();
});
it('has 3 links', () => {
expect(aTags.length === 3).toBeTruthy();
expect(
result.props.children[1].props['data-test-label'] === 'landing-small-cta'
).toBeTruthy();
});
it('has links to news, forum, learn and portfolio', () => {
// checks if all links in expected links exist in links
expect(expectedLinks.every(elem => links.indexOf(elem) > -1)).toBeTruthy();
it('has Curriculum and Portfolio links when user signed in on /learn', () => {
const defaultUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
isDonating: true
},
pending: false,
pathName: '/learn'
};
const shallow = new ShallowRenderer();
shallow.render(<AuthOrProfile {...defaultUserProps} />);
const result = shallow.getRenderOutput();
expect(hasCurriculumNavItem(result)).toBeTruthy();
expect(hasProfileNavItem(result)).toBeTruthy();
});
it('has avatar with default border for default users', () => {
const defaultUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png'
},
pending: false,
pathName: '/learn'
};
const componentTree = renderer
.create(<AuthOrProfile {...defaultUserProps} />)
.toJSON();
expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy();
});
it('has avatar with gold border for donating users', () => {
const donatingUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
isDonating: true
},
pending: false,
pathName: '/learn'
};
const componentTree = renderer
.create(<AuthOrProfile {...donatingUserProps} />)
.toJSON();
expect(avatarHasClass(componentTree, 'gold-border')).toBeTruthy();
});
it('has avatar with green border for top contributors', () => {
const topContributorUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
yearsTopContributor: [2020]
},
pending: false,
pathName: '/learn'
};
const componentTree = renderer
.create(<AuthOrProfile {...topContributorUserProps} />)
.toJSON();
expect(avatarHasClass(componentTree, 'green-border')).toBeTruthy();
});
it('has avatar with purple border for donating top contributors', () => {
const topDonatingContributorUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
isDonating: true,
yearsTopContributor: [2020]
},
pending: false,
pathName: '/learn'
};
const componentTree = renderer
.create(<AuthOrProfile {...topDonatingContributorUserProps} />)
.toJSON();
expect(avatarHasClass(componentTree, 'purple-border')).toBeTruthy();
});
});
const UniversalNavProps = {
displayMenu: false,
menuButtonRef: {},
searchBarRef: {},
toggleDisplayMenu: function() {}
const deepChildrenProp = (component, childNumber) =>
component.props.children[childNumber].props.children.props;
const hasProfileNavItem = component => {
const profileElement = deepChildrenProp(component, 1);
return (
profileElement.children[0] === 'Profile' &&
profileElement.to === '/test-user'
);
};
const hasCurriculumNavItem = component => {
const curriculumElement = deepChildrenProp(component, 0);
return (
curriculumElement.children === 'Curriculum' &&
curriculumElement.to === '/learn'
);
};
const avatarHasClass = (componentTree, classes) => {
return (
componentTree[1].children[0].children[1].props.className ===
'avatar-container ' + classes
);
};

View File

@ -0,0 +1,65 @@
/* eslint-disable react/sort-prop-types */
import React from 'react';
import {
Link,
borderColorPicker,
SkeletonSprite,
AvatarRenderer
} from '../../helpers';
import PropTypes from 'prop-types';
import Login from '../components/Login';
const propTypes = {
pending: PropTypes.bool,
pathName: PropTypes.string.isRequired,
user: PropTypes.object
};
export function AuthOrProfile({ user, pathName, pending }) {
const isUserDonating = user && user.isDonating;
const isUserSignedIn = user && user.username;
const isTopContributor =
user && user.yearsTopContributor && user.yearsTopContributor.length > 0;
const badgeColorClass = borderColorPicker(isUserDonating, isTopContributor);
if (pending && pathName !== '/') {
return (
<div className='nav-skeleton'>
<SkeletonSprite />
</div>
);
} else if (pathName === '/' || !isUserSignedIn) {
return <Login data-test-label='landing-small-cta'>Sign In</Login>;
} else {
return (
<>
<li>
<Link className='nav-link' to='/learn'>
Curriculum
</Link>
</li>
<li>
<Link className='nav-link' to={`/${user.username}`}>
Profile
</Link>
<Link
className={`avatar-nav-link ${badgeColorClass}`}
to={`/${user.username}`}
>
<AvatarRenderer
picture={user.picture}
size='sm'
userName={user.username}
/>
</Link>
</li>
</>
);
}
}
AuthOrProfile.propTypes = propTypes;
AuthOrProfile.displayName = 'AuthOrProfile';
export default AuthOrProfile;

View File

@ -1,30 +1,68 @@
import React from 'react';
import { Link } from '../../helpers';
import { forumLocation } from '../../../../../config/env.json';
import { Link, SkeletonSprite, AvatarRenderer } from '../../helpers';
import PropTypes from 'prop-types';
import Login from '../components/Login';
const propTypes = {
displayMenu: PropTypes.bool
displayMenu: PropTypes.bool,
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
pathName: PropTypes.string.isRequired,
user: PropTypes.object
};
function NavLinks({ displayMenu }) {
export function AuthOrProfile({ user, pathName, pending }) {
const isUserDonating = user && user.isDonating;
const isUserSignedIn = user && user.username;
const isTopContributor =
user && user.yearsTopContributor && user.yearsTopContributor.length > 0;
if (pending && pathName !== '/') {
return (
<div className='nav-skeleton'>
<SkeletonSprite />
</div>
);
} else if (pathName === '/' || !isUserSignedIn) {
return (
<>
<li>
<Link className='nav-link' to='/learn'>
Curriculum
</Link>
</li>
<Login data-test-label='landing-small-cta'>Sign In</Login>
</>
);
} else {
return (
<>
<li>
<Link className='nav-link' to='/learn'>
Curriculum
</Link>
</li>
<li>
<Link className='nav-link' to={`/${user.username}`}>
Profile
<AvatarRenderer
isDonating={isUserDonating}
isTopContributor={isTopContributor}
picture={user.picture}
userName={user.username}
/>
</Link>
</li>
</>
);
}
}
export function NavLinks({ displayMenu, pathName, user, fetchState }) {
const { pending } = fetchState;
return (
<div className='main-nav-group'>
<ul className={'nav-list' + (displayMenu ? ' display-flex' : '')}>
<li className='nav-news'>
<Link external={true} sameTab={true} to='/news'>
/news
</Link>
</li>
<li className='nav-forum'>
<Link external={true} sameTab={true} to={forumLocation}>
/forum
</Link>
</li>
<li className='nav-projects'>
<Link to='/learn'>/learn</Link>
</li>
<AuthOrProfile pathName={pathName} pending={pending} user={user} />
</ul>
</div>
);

View File

@ -12,7 +12,10 @@ export const UniversalNav = ({
displayMenu,
toggleDisplayMenu,
menuButtonRef,
searchBarRef
searchBarRef,
pathName,
user,
fetchState
}) => (
<nav
className={'universal-nav nav-padding' + (displayMenu ? ' expand-nav' : '')}
@ -30,7 +33,12 @@ export const UniversalNav = ({
</Link>
</div>
<div className='universal-nav-right main-nav'>
<NavLinks displayMenu={displayMenu} />
<NavLinks
displayMenu={displayMenu}
fetchState={fetchState}
pathName={pathName}
user={user}
/>
</div>
<MenuButton
displayMenu={displayMenu}
@ -45,7 +53,10 @@ export default UniversalNav;
UniversalNav.propTypes = {
displayMenu: PropTypes.bool,
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
menuButtonRef: PropTypes.object,
pathName: PropTypes.string.isRequired,
searchBarRef: PropTypes.object,
toggleDisplayMenu: PropTypes.func
toggleDisplayMenu: PropTypes.func,
user: PropTypes.object
};

View File

@ -68,10 +68,11 @@
margin: 0 0 0 -12px;
padding: 0;
list-style: none;
align-items: center;
}
.nav-list {
height: 38px;
height: var(--header-height);
}
.nav-list li {
@ -80,37 +81,66 @@
padding: 0;
}
.nav-list li a {
display: block;
.nav-list li a.nav-link {
display: flex;
margin: 0;
padding: 6px 15px;
padding: 0 15px;
color: var(--gray-00);
opacity: 1;
white-space: nowrap;
height: 38px;
height: var(--header-height);
align-items: center;
}
.nav-list li a:hover {
.nav-list li:last-child {
display: flex;
flex-direction: row;
}
.nav-list li:last-child a {
padding-right: 15px;
}
.nav-list li a.nav-link:hover {
color: var(--theme-color);
text-decoration: none;
background: white;
}
.nav-list li a:focus {
.nav-list li a.nav-link:focus {
background: var(--theme-color);
color: white;
}
.nav-list li a:focus:hover {
.nav-list li a.nav-link:focus:hover {
color: var(--theme-color);
background: white;
}
.nav-list a.btn-cta {
padding: 0 20px;
height: 30px;
margin-left: 19px;
margin-right: 15px;
}
.nav-skeleton {
height: var(--header-height);
margin-right: 15px;
width: 200px;
}
.nav-list .fcc-loader {
padding: 0 40px;
margin-left: 35px;
margin-right: 25px;
}
.universal-nav-right {
flex-shrink: 0;
display: flex;
align-items: center;
height: 38px;
height: var(--header-height);
}
.toggle-button-nav {
@ -133,8 +163,46 @@
border: 1px solid var(--gray-00);
}
.nav-list li .avatar-container {
display: block;
padding: 0;
margin-left: 15px;
opacity: 1;
white-space: nowrap;
background: transparent;
height: calc(var(--header-height) - 8px);
width: calc(var(--header-height) - 8px);
}
.nav-list li .avatar-containersvg {
display: inline-block;
background: var(--secondary-background);
}
.nav-list .avatar-container svg,
.nav-list .avatar-container img {
object-fit: cover;
width: 100%;
height: 100%;
}
.nav-list .gold-border {
border: 2px solid var(--yellow-gold);
}
.nav-list .green-border {
border: 2px solid var(--green-mid);
}
.nav-list .purple-border {
border: 2px solid var(--purple-mid);
}
.nav-list .default-border {
border: 2px solid transparent;
}
@media (max-width: 300px) {
.nav-list li a {
.nav-list li a.nav-link {
width: 50vw;
text-align: center;
}

View File

@ -1,11 +1,18 @@
import React from 'react';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
import stripeObserver from './stripeIframesFix';
import UniversalNav from './components/UniversalNav';
import './header.css';
const propTypes = {
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
pathName: PropTypes.string.isRequired,
user: PropTypes.object
};
export class Header extends React.Component {
constructor(props) {
super(props);
@ -47,6 +54,7 @@ export class Header extends React.Component {
}
render() {
const { displayMenu } = this.state;
const { fetchState, pathName, user } = this.props;
return (
<>
<Helmet>
@ -55,9 +63,12 @@ export class Header extends React.Component {
<header>
<UniversalNav
displayMenu={displayMenu}
fetchState={fetchState}
menuButtonRef={this.menuButtonRef}
pathName={pathName}
searchBarRef={this.searchBarRef}
toggleDisplayMenu={this.toggleDisplayMenu}
user={user}
/>
</header>
</>
@ -65,5 +76,7 @@ export class Header extends React.Component {
}
}
Header.propTypes = propTypes;
Header.displayName = 'Header';
export default Header;

View File

@ -21,7 +21,6 @@ const propTypes = {
function Intro({
isSignedIn,
username,
name,
pending,
complete,
@ -49,13 +48,7 @@ function Intro({
<Spacer />
</Col>
</Row>
<FullWidthRow className='button-group'>
<Link
className='btn btn-lg btn-primary btn-block'
to={`/${username}`}
>
View my Portfolio
</Link>
<FullWidthRow>
<Link className='btn btn-lg btn-primary btn-block' to='/settings'>
Update my account settings
</Link>

View File

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Image } from '@freecodecamp/react-bootstrap';
import DefaultAvatar from '../../assets/icons/DefaultAvatar';
import { defaultUserImage } from '../../../../config/misc';
const propTypes = {
isDonating: PropTypes.bool,
isTopContributor: PropTypes.bool,
picture: PropTypes.any.isRequired,
userName: PropTypes.string.isRequired
};
function borderColorPicker(isDonating, isTopContributor) {
if (isDonating && isTopContributor) return 'purple-border';
else if (isTopContributor) return 'green-border';
else if (isDonating) return 'gold-border';
else return 'default-border';
}
function AvatarRenderer({ picture, userName, isDonating, isTopContributor }) {
let borderColor = borderColorPicker(isDonating, isTopContributor);
let isPlaceHolderImage =
/example.com|identicon.org/.test(picture) || picture === defaultUserImage;
return (
<div className={`avatar-container ${borderColor}`}>
{isPlaceHolderImage ? (
<DefaultAvatar className='avatar default-avatar' />
) : (
<Image
alt={userName + "'s avatar"}
className='avatar'
responsive={true}
src={picture}
/>
)}
</div>
);
}
AvatarRenderer.propTypes = propTypes;
AvatarRenderer.displayName = 'AvatarRenderer';
export default AvatarRenderer;

View File

@ -9,9 +9,9 @@ function SkeletonSprite() {
<svg className='sprite-svg'>
<rect
className='sprite'
fill='#ccc'
fill='var(--gray-75)'
height='100%'
stroke='#ccc'
stroke='var(--gray-75)'
width='2px'
x='0'
y='0'

View File

@ -0,0 +1,6 @@
export default function borderColorPicker(isDonating, isTopContributor) {
if (isDonating && isTopContributor) return 'purple-border';
else if (isTopContributor) return 'green-border';
else if (isDonating) return 'gold-border';
else return 'default-border';
}

View File

@ -7,3 +7,5 @@ export { default as Spacer } from './Spacer';
export { default as Link } from './Link';
export { default as CurrentChallengeLink } from './CurrentChallengeLink';
export { default as ImageLoader } from './ImageLoader';
export { default as AvatarRenderer } from './AvatarRenderer';
export { default as borderColorPicker } from './borderColorPicker';

View File

@ -8,7 +8,7 @@ export default `
.sprite-svg {
height: 100%;
width: 100%;
background: #aaa;
background: var(--theme-color);
}
@ -18,8 +18,15 @@ export default `
transform: translateX(0%);
stroke-width: 2px;
}
5%{
opacity:100%;
}
35% {
stroke-width: 30px;
opacity:100%;
}
65%{
opacity:100%;
}
100% {
-webkit-transform: translateX(100%);
@ -36,6 +43,10 @@ export default `
}
35% {
stroke-width: 30px;
opacity:100%;
}
65%{
opacity:100%;
}
100% {
-webkit-transform: translateX(100%);
@ -45,8 +56,10 @@ export default `
}
.sprite {
opacity:0%;
-webkit-animation-name: shimmer;
animation-name: shimmer;
animation-delay: 1s;
width: 2px;
-webkit-animation-duration: 2s;
animation-duration: 2s;

View File

@ -11,7 +11,9 @@ import {
isSignedInSelector,
onlineStatusChange,
isOnlineSelector,
userFetchStateSelector,
userSelector,
usernameSelector,
executeGA
} from '../../redux';
import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
@ -69,6 +71,7 @@ const metaKeywords = [
const propTypes = {
children: PropTypes.node.isRequired,
executeGA: PropTypes.func,
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
fetchUser: PropTypes.func.isRequired,
flashMessage: PropTypes.shape({
id: PropTypes.string,
@ -82,21 +85,27 @@ const propTypes = {
pathname: PropTypes.string.isRequired,
removeFlashMessage: PropTypes.func.isRequired,
showFooter: PropTypes.bool,
signedInUserName: PropTypes.string,
theme: PropTypes.string,
useTheme: PropTypes.bool
useTheme: PropTypes.bool,
user: PropTypes.object
};
const mapStateToProps = createSelector(
isSignedInSelector,
flashMessageSelector,
isOnlineSelector,
userFetchStateSelector,
userSelector,
(isSignedIn, flashMessage, isOnline, user) => ({
usernameSelector,
(isSignedIn, flashMessage, isOnline, fetchState, user) => ({
isSignedIn,
flashMessage,
hasMessage: !!flashMessage.message,
isOnline,
theme: user.theme
fetchState,
theme: user.theme,
user
})
);
@ -142,14 +151,18 @@ class DefaultLayout extends Component {
const {
children,
hasMessage,
fetchState,
flashMessage,
isOnline,
isSignedIn,
removeFlashMessage,
showFooter = true,
theme = 'default',
useTheme = true
user,
useTheme = true,
pathname
} = this.props;
return (
<Fragment>
<Helmet
@ -213,7 +226,7 @@ class DefaultLayout extends Component {
<style>{fontawesome.dom.css()}</style>
</Helmet>
<WithInstantSearch>
<Header />
<Header fetchState={fetchState} pathName={pathname} user={user} />
<div className={`default-layout`}>
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
{hasMessage && flashMessage ? (

View File

@ -225,7 +225,7 @@ fieldset[disabled] .btn-primary.focus {
.btn-cta:hover,
.btn-cta:focus,
.btn-cta:active:hover {
background-color: #fecc4c;
background-color: #fecc4c !important;
border-width: 3px;
border-color: #f1a02a;
background-image: none;

View File

@ -17,6 +17,8 @@
--blue-light: #99c9ff;
--blue-dark: #002ead;
--green-light: #acd157;
--green-mid: green;
--purple-mid: darkviolet;
--green-dark: #00471b;
--red-light: #ffadad;
--red-dark: #850000;

View File

@ -21,7 +21,7 @@ exports[`<Profile/> renders correctly 1`] = `
class="avatar-container col-xs-12"
>
<div
class=""
class="avatar-container default-border"
>
<img
alt="string's avatar"

View File

@ -1,13 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Col, Row, Image } from '@freecodecamp/react-bootstrap';
import { Col, Row } from '@freecodecamp/react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faAward,
faHeart,
faCalendar
} from '@fortawesome/free-solid-svg-icons';
import Identicon from 'react-identicons';
import { AvatarRenderer } from '../../helpers';
import SocialIcons from './SocialIcons';
@ -77,29 +78,16 @@ function Camper({
twitter,
website
}) {
// A lot of the user-profiles are still using the defunct service.
const avatar = /example.com|identicon.org/.test(picture) ? (
<Identicon
bg={'#858591'}
count={5}
fg={'#0A0A23'}
padding={5}
size={256}
string={username}
/>
) : (
<Image
alt={username + "'s avatar"}
className='avatar'
responsive={true}
src={picture}
/>
);
return (
<div>
<Row>
<Col className='avatar-container' xs={12}>
<div className={isDonating ? 'supporter-img' : ''}>{avatar}</div>
<AvatarRenderer
isDonating={isDonating}
isTopContributor={yearsTopContributor.length > 0}
picture={picture}
userName={username}
/>
</Col>
</Row>
<SocialIcons

View File

@ -1,5 +1,7 @@
.avatar-container .avatar {
height: 180px;
width: 180px;
object-fit: cover;
}
.avatar-container {
@ -14,6 +16,22 @@
overflow-wrap: break-word;
}
.supporter-img {
.avatar-container div {
height: 200px;
}
.avatar-container .gold-border {
border: 10px solid var(--yellow-gold);
}
.avatar-container .green-border {
border: 10px solid var(--green-mid);
}
.avatar-container .purple-border {
border: 10px solid var(--purple-mid);
}
.avatar-container .default-border {
border: 10px solid transparent;
}

View File

@ -65,7 +65,7 @@ export const LearnPage = ({
location: { hash = '', state = '' },
isSignedIn,
fetchState: { pending, complete },
user: { name = '', username = '', completedChallengeCount = 0 },
user: { name = '', completedChallengeCount = 0 },
data: {
challengeNode: {
fields: { slug }
@ -86,7 +86,6 @@ export const LearnPage = ({
name={name}
pending={pending}
slug={slug}
username={username}
/>
<Map
hash={hashValue}

View File

@ -1 +1,2 @@
exports.oldDataVizId = '561add10cb82ac38a17513b3';
exports.defaultUserImage = 'https://freecodecamp.com/sample-image.png';

View File

@ -1,5 +1,14 @@
/* global cy */
const selectors = {
heading: "[data-test-label='landing-header']",
smallCallToAction: "[data-test-label='landing-small-cta']",
firstNavigationLink: '.nav-list .nav-link:first-child',
lastNavigationLink: '.nav-list .nav-link:last-child',
avatarContainer: '.avatar-container',
defaultAvatar: '.avatar-container svg'
};
describe('Navbar', () => {
beforeEach(() => {
cy.visit('/');
@ -10,32 +19,6 @@ describe('Navbar', () => {
cy.get('#universal-nav').should('have.class', 'universal-nav nav-padding');
});
it('Should take user to news page when clicked on `/news`', () => {
cy.get('.nav-news').within(() => {
cy.contains('/news').click();
});
cy.url().should('include', '/news');
});
it('Should take user to forum page when clicked on `/forum`', () => {
cy.get('.nav-forum').within(() => {
// Can't click on it in test due to CORS policy
// So check the link instead
cy.contains('/forum').should(
'have.attr',
'href',
'https://forum.freecodecamp.org'
);
});
});
it('Should take user to learn page when clicked on `/learn`', () => {
cy.get('.nav-projects').within(() => {
cy.contains('/learn').click();
});
cy.url().should('include', '/learn');
});
it(
'Should take user to learn page when clicked on ' + 'the freeCodeCamp logo',
() => {
@ -53,7 +36,7 @@ describe('Navbar', () => {
cy.get('.ais-Hits-list')
.children()
.should('have.length', 1);
.should('to.have.length.of.at.least', 1);
cy.get('.ais-SearchBox').within(() => {
cy.get('input').clear();
@ -61,4 +44,48 @@ describe('Navbar', () => {
cy.get('div.ais-Hits').should('not.be.visible');
});
it('Should have a Sign In button', () => {
cy.contains("[data-test-label='landing-small-cta']", 'Sign In');
});
// have the curriculum and CTA on landing and /learn pages.
it(
'Should have `Curriculum` link on landing and learn pages' +
'page when not signed in',
() => {
cy.get(selectors.firstNavigationLink)
.contains('Curriculum')
.click();
cy.url().should('include', '/learn');
cy.get(selectors.firstNavigationLink).contains('Curriculum');
}
);
it(
'Should have `Sign In` link on landing and learn pages' +
'page when not signed in',
() => {
cy.contains(selectors.smallCallToAction, 'Sign In');
cy.get(selectors.firstNavigationLink)
.contains('Curriculum')
.click();
cy.contains(selectors.smallCallToAction, 'Sign In');
}
);
it('Should have `Profile` link when user is signed in', () => {
cy.login()
.get(selectors.lastNavigationLink)
.contains('Profile')
.click();
cy.url().should('include', '/developmentuser');
});
it('Should have a profile image with class `default-border`', () => {
cy.login()
.get(selectors.avatarContainer)
.should('have.class', 'default-border');
cy.get(selectors.defaultAvatar).should('exist');
});
});

View File

@ -2,18 +2,7 @@
describe('The `Update my account settings` button works properly', function() {
beforeEach(() => {
cy.visit('/');
cy.contains("Get started (it's free)").click();
});
it('Should get rendered', function() {
cy.contains('View my Portfolio').should(
'have.class',
'btn btn-lg btn-primary btn-block'
);
cy.contains('View my Portfolio').should('be.visible');
cy.login();
});
it('Should take user to their account settings when clicked', function() {

View File

@ -1,23 +0,0 @@
/* global cy */
describe('The `View my Portfolio` button works properly', function() {
beforeEach(() => {
cy.visit('/');
cy.contains("Get started (it's free)").click();
});
it('Button gets rendered', function() {
cy.contains('View my Portfolio').should(
'have.class',
'btn btn-lg btn-primary btn-block'
);
cy.contains('View my Portfolio').should('be.visible');
});
it('Button takes user to their portfolio when clicked', function() {
cy.contains('View my Portfolio').click();
cy.url().should('include', '/developmentuser');
});
});

View File

@ -1,3 +1,4 @@
/* global cy Cypress*/
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
@ -31,3 +32,8 @@
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => {});
Cypress.Commands.add('login', () => {
cy.visit('/');
cy.contains("Get started (it's free)").click({ force: true });
});

View File

@ -7,6 +7,8 @@ const debug = require('debug');
const log = debug('fcc:tools:seedLocalAuthUser');
const { MONGOHQ_URL } = process.env;
const defaulUserImage = require('../../../config/misc').defaulUserImage;
function handleError(err, client) {
if (err) {
console.error('Oh noes!! Error seeding local auth user.');
@ -48,7 +50,7 @@ MongoClient.connect(MONGOHQ_URL, { useNewUrlParser: true }, function(
about: '',
name: 'Development User',
location: '',
picture: 'https://github.com/identicons/camperbot.png',
picture: defaulUserImage,
acceptedPrivacyTerms: true,
sendQuincyEmail: false,
currentChallengeId: '',