feat: render nav conditionally
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
422bacd15d
commit
1a66eac990
9
client/package-lock.json
generated
9
client/package-lock.json
generated
@ -21295,15 +21295,6 @@
|
|||||||
"camelcase": "^5.0.0"
|
"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": {
|
"react-instantsearch-core": {
|
||||||
"version": "6.7.0",
|
"version": "6.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.7.0.tgz",
|
||||||
|
@ -56,7 +56,6 @@
|
|||||||
"react-ga": "^2.7.0",
|
"react-ga": "^2.7.0",
|
||||||
"react-helmet": "^5.2.1",
|
"react-helmet": "^5.2.1",
|
||||||
"react-hotkeys": "^2.0.0",
|
"react-hotkeys": "^2.0.0",
|
||||||
"react-identicons": "^1.1.7",
|
|
||||||
"react-instantsearch-dom": "^6.7.0",
|
"react-instantsearch-dom": "^6.7.0",
|
||||||
"react-lazy-load": "^3.1.13",
|
"react-lazy-load": "^3.1.13",
|
||||||
"react-monaco-editor": "^0.36.0",
|
"react-monaco-editor": "^0.36.0",
|
||||||
|
91
client/src/assets/icons/DefaultAvatar.js
Normal file
91
client/src/assets/icons/DefaultAvatar.js
Normal 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;
|
@ -15,7 +15,7 @@ import {
|
|||||||
import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
|
import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
|
||||||
import { createFlashMessage } from '../components/Flash/redux';
|
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 About from '../components/settings/About';
|
||||||
import Privacy from '../components/settings/Privacy';
|
import Privacy from '../components/settings/Privacy';
|
||||||
import Email from '../components/settings/Email';
|
import Email from '../components/settings/Email';
|
||||||
@ -177,13 +177,7 @@ export function ShowSettings(props) {
|
|||||||
<Grid>
|
<Grid>
|
||||||
<main>
|
<main>
|
||||||
<Spacer size={2} />
|
<Spacer size={2} />
|
||||||
<FullWidthRow className='button-group'>
|
<FullWidthRow>
|
||||||
<Link
|
|
||||||
className='btn-invert btn btn-lg btn-primary btn-block'
|
|
||||||
to={`/${username}`}
|
|
||||||
>
|
|
||||||
Show me my public portfolio
|
|
||||||
</Link>
|
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
bsSize='lg'
|
bsSize='lg'
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
/* global expect */
|
/* global expect */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||||
import TestRenderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { UniversalNav } from './components/UniversalNav';
|
|
||||||
import NavLinks from './components/NavLinks';
|
|
||||||
|
|
||||||
import { forumLocation } from '../../../config/env.json';
|
import { UniversalNav } from './components/UniversalNav';
|
||||||
|
import { AuthOrProfile } from './components/NavLinks';
|
||||||
|
|
||||||
describe('<UniversalNav />', () => {
|
describe('<UniversalNav />', () => {
|
||||||
|
const UniversalNavProps = {
|
||||||
|
displayMenu: false,
|
||||||
|
menuButtonRef: {},
|
||||||
|
searchBarRef: {},
|
||||||
|
toggleDisplayMenu: function() {},
|
||||||
|
pathName: '/'
|
||||||
|
};
|
||||||
it('renders to the DOM', () => {
|
it('renders to the DOM', () => {
|
||||||
const shallow = new ShallowRenderer();
|
const shallow = new ShallowRenderer();
|
||||||
shallow.render(<UniversalNav {...UniversalNavProps} />);
|
shallow.render(<UniversalNav {...UniversalNavProps} />);
|
||||||
@ -15,34 +21,138 @@ describe('<UniversalNav />', () => {
|
|||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<NavLinks />', () => {
|
describe('<NavLinks />', () => {
|
||||||
const root = TestRenderer.create(<NavLinks />).root;
|
it('shows Curriculum and Sign In buttons on landing page', () => {
|
||||||
const aTags = root.findAllByType('a');
|
const landingPageProps = {
|
||||||
|
user: {
|
||||||
// reduces the aTags to href links
|
username: 'test-user',
|
||||||
const links = aTags.reduce((acc, item) => {
|
picture: 'https://freecodecamp.org/image.png'
|
||||||
acc.push(item._fiber.pendingProps.href);
|
},
|
||||||
return acc;
|
pending: false,
|
||||||
}, []);
|
pathName: '/'
|
||||||
|
};
|
||||||
const expectedLinks = ['/learn', '/news', forumLocation];
|
const shallow = new ShallowRenderer();
|
||||||
|
shallow.render(<AuthOrProfile {...landingPageProps} />);
|
||||||
it('renders to the DOM', () => {
|
const result = shallow.getRenderOutput();
|
||||||
expect(root).toBeTruthy();
|
// expect(result.props.children).toEqual('Sign In');
|
||||||
});
|
|
||||||
it('has 3 links', () => {
|
expect(deepChildrenProp(result, 0).children === 'Curriculum').toBeTruthy();
|
||||||
expect(aTags.length === 3).toBeTruthy();
|
|
||||||
});
|
expect(
|
||||||
|
result.props.children[1].props['data-test-label'] === 'landing-small-cta'
|
||||||
it('has links to news, forum, learn and portfolio', () => {
|
).toBeTruthy();
|
||||||
// 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: {
|
||||||
const UniversalNavProps = {
|
username: 'test-user',
|
||||||
displayMenu: false,
|
picture: 'https://freecodecamp.org/image.png',
|
||||||
menuButtonRef: {},
|
isDonating: true
|
||||||
searchBarRef: {},
|
},
|
||||||
toggleDisplayMenu: function() {}
|
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 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
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
65
client/src/components/Header/components/AuthOrProfile.js
Normal file
65
client/src/components/Header/components/AuthOrProfile.js
Normal 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;
|
@ -1,30 +1,68 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from '../../helpers';
|
import { Link, SkeletonSprite, AvatarRenderer } from '../../helpers';
|
||||||
import { forumLocation } from '../../../../../config/env.json';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Login from '../components/Login';
|
||||||
|
|
||||||
const propTypes = {
|
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 (
|
return (
|
||||||
<div className='main-nav-group'>
|
<div className='main-nav-group'>
|
||||||
<ul className={'nav-list' + (displayMenu ? ' display-flex' : '')}>
|
<ul className={'nav-list' + (displayMenu ? ' display-flex' : '')}>
|
||||||
<li className='nav-news'>
|
<AuthOrProfile pathName={pathName} pending={pending} user={user} />
|
||||||
<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>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -12,7 +12,10 @@ export const UniversalNav = ({
|
|||||||
displayMenu,
|
displayMenu,
|
||||||
toggleDisplayMenu,
|
toggleDisplayMenu,
|
||||||
menuButtonRef,
|
menuButtonRef,
|
||||||
searchBarRef
|
searchBarRef,
|
||||||
|
pathName,
|
||||||
|
user,
|
||||||
|
fetchState
|
||||||
}) => (
|
}) => (
|
||||||
<nav
|
<nav
|
||||||
className={'universal-nav nav-padding' + (displayMenu ? ' expand-nav' : '')}
|
className={'universal-nav nav-padding' + (displayMenu ? ' expand-nav' : '')}
|
||||||
@ -30,7 +33,12 @@ export const UniversalNav = ({
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='universal-nav-right main-nav'>
|
<div className='universal-nav-right main-nav'>
|
||||||
<NavLinks displayMenu={displayMenu} />
|
<NavLinks
|
||||||
|
displayMenu={displayMenu}
|
||||||
|
fetchState={fetchState}
|
||||||
|
pathName={pathName}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
displayMenu={displayMenu}
|
displayMenu={displayMenu}
|
||||||
@ -45,7 +53,10 @@ export default UniversalNav;
|
|||||||
|
|
||||||
UniversalNav.propTypes = {
|
UniversalNav.propTypes = {
|
||||||
displayMenu: PropTypes.bool,
|
displayMenu: PropTypes.bool,
|
||||||
|
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
|
||||||
menuButtonRef: PropTypes.object,
|
menuButtonRef: PropTypes.object,
|
||||||
|
pathName: PropTypes.string.isRequired,
|
||||||
searchBarRef: PropTypes.object,
|
searchBarRef: PropTypes.object,
|
||||||
toggleDisplayMenu: PropTypes.func
|
toggleDisplayMenu: PropTypes.func,
|
||||||
|
user: PropTypes.object
|
||||||
};
|
};
|
||||||
|
@ -68,10 +68,11 @@
|
|||||||
margin: 0 0 0 -12px;
|
margin: 0 0 0 -12px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
height: 38px;
|
height: var(--header-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list li {
|
.nav-list li {
|
||||||
@ -80,37 +81,66 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list li a {
|
.nav-list li a.nav-link {
|
||||||
display: block;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 6px 15px;
|
padding: 0 15px;
|
||||||
color: var(--gray-00);
|
color: var(--gray-00);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
white-space: nowrap;
|
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);
|
color: var(--theme-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list li a:focus {
|
.nav-list li a.nav-link:focus {
|
||||||
background: var(--theme-color);
|
background: var(--theme-color);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list li a:focus:hover {
|
.nav-list li a.nav-link:focus:hover {
|
||||||
color: var(--theme-color);
|
color: var(--theme-color);
|
||||||
background: white;
|
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 {
|
.universal-nav-right {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 38px;
|
height: var(--header-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-button-nav {
|
.toggle-button-nav {
|
||||||
@ -133,8 +163,46 @@
|
|||||||
border: 1px solid var(--gray-00);
|
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) {
|
@media (max-width: 300px) {
|
||||||
.nav-list li a {
|
.nav-list li a.nav-link {
|
||||||
width: 50vw;
|
width: 50vw;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import stripeObserver from './stripeIframesFix';
|
import stripeObserver from './stripeIframesFix';
|
||||||
import UniversalNav from './components/UniversalNav';
|
import UniversalNav from './components/UniversalNav';
|
||||||
|
|
||||||
import './header.css';
|
import './header.css';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
|
||||||
|
pathName: PropTypes.string.isRequired,
|
||||||
|
user: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
export class Header extends React.Component {
|
export class Header extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -47,6 +54,7 @@ export class Header extends React.Component {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { displayMenu } = this.state;
|
const { displayMenu } = this.state;
|
||||||
|
const { fetchState, pathName, user } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -55,9 +63,12 @@ export class Header extends React.Component {
|
|||||||
<header>
|
<header>
|
||||||
<UniversalNav
|
<UniversalNav
|
||||||
displayMenu={displayMenu}
|
displayMenu={displayMenu}
|
||||||
|
fetchState={fetchState}
|
||||||
menuButtonRef={this.menuButtonRef}
|
menuButtonRef={this.menuButtonRef}
|
||||||
|
pathName={pathName}
|
||||||
searchBarRef={this.searchBarRef}
|
searchBarRef={this.searchBarRef}
|
||||||
toggleDisplayMenu={this.toggleDisplayMenu}
|
toggleDisplayMenu={this.toggleDisplayMenu}
|
||||||
|
user={user}
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
</>
|
</>
|
||||||
@ -65,5 +76,7 @@ export class Header extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Header.propTypes = propTypes;
|
||||||
Header.displayName = 'Header';
|
Header.displayName = 'Header';
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
@ -21,7 +21,6 @@ const propTypes = {
|
|||||||
|
|
||||||
function Intro({
|
function Intro({
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
username,
|
|
||||||
name,
|
name,
|
||||||
pending,
|
pending,
|
||||||
complete,
|
complete,
|
||||||
@ -49,13 +48,7 @@ function Intro({
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<FullWidthRow className='button-group'>
|
<FullWidthRow>
|
||||||
<Link
|
|
||||||
className='btn btn-lg btn-primary btn-block'
|
|
||||||
to={`/${username}`}
|
|
||||||
>
|
|
||||||
View my Portfolio
|
|
||||||
</Link>
|
|
||||||
<Link className='btn btn-lg btn-primary btn-block' to='/settings'>
|
<Link className='btn btn-lg btn-primary btn-block' to='/settings'>
|
||||||
Update my account settings
|
Update my account settings
|
||||||
</Link>
|
</Link>
|
||||||
|
44
client/src/components/helpers/AvatarRenderer.js
Normal file
44
client/src/components/helpers/AvatarRenderer.js
Normal 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;
|
@ -9,9 +9,9 @@ function SkeletonSprite() {
|
|||||||
<svg className='sprite-svg'>
|
<svg className='sprite-svg'>
|
||||||
<rect
|
<rect
|
||||||
className='sprite'
|
className='sprite'
|
||||||
fill='#ccc'
|
fill='var(--gray-75)'
|
||||||
height='100%'
|
height='100%'
|
||||||
stroke='#ccc'
|
stroke='var(--gray-75)'
|
||||||
width='2px'
|
width='2px'
|
||||||
x='0'
|
x='0'
|
||||||
y='0'
|
y='0'
|
||||||
|
6
client/src/components/helpers/borderColorPicker.js
Normal file
6
client/src/components/helpers/borderColorPicker.js
Normal 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';
|
||||||
|
}
|
@ -7,3 +7,5 @@ export { default as Spacer } from './Spacer';
|
|||||||
export { default as Link } from './Link';
|
export { default as Link } from './Link';
|
||||||
export { default as CurrentChallengeLink } from './CurrentChallengeLink';
|
export { default as CurrentChallengeLink } from './CurrentChallengeLink';
|
||||||
export { default as ImageLoader } from './ImageLoader';
|
export { default as ImageLoader } from './ImageLoader';
|
||||||
|
export { default as AvatarRenderer } from './AvatarRenderer';
|
||||||
|
export { default as borderColorPicker } from './borderColorPicker';
|
||||||
|
@ -8,7 +8,7 @@ export default `
|
|||||||
.sprite-svg {
|
.sprite-svg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #aaa;
|
background: var(--theme-color);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,8 +18,15 @@ export default `
|
|||||||
transform: translateX(0%);
|
transform: translateX(0%);
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
}
|
}
|
||||||
|
5%{
|
||||||
|
opacity:100%;
|
||||||
|
}
|
||||||
35% {
|
35% {
|
||||||
stroke-width: 30px;
|
stroke-width: 30px;
|
||||||
|
opacity:100%;
|
||||||
|
}
|
||||||
|
65%{
|
||||||
|
opacity:100%;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
-webkit-transform: translateX(100%);
|
-webkit-transform: translateX(100%);
|
||||||
@ -36,6 +43,10 @@ export default `
|
|||||||
}
|
}
|
||||||
35% {
|
35% {
|
||||||
stroke-width: 30px;
|
stroke-width: 30px;
|
||||||
|
opacity:100%;
|
||||||
|
}
|
||||||
|
65%{
|
||||||
|
opacity:100%;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
-webkit-transform: translateX(100%);
|
-webkit-transform: translateX(100%);
|
||||||
@ -45,8 +56,10 @@ export default `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sprite {
|
.sprite {
|
||||||
|
opacity:0%;
|
||||||
-webkit-animation-name: shimmer;
|
-webkit-animation-name: shimmer;
|
||||||
animation-name: shimmer;
|
animation-name: shimmer;
|
||||||
|
animation-delay: 1s;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
-webkit-animation-duration: 2s;
|
-webkit-animation-duration: 2s;
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
|
@ -11,7 +11,9 @@ import {
|
|||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
onlineStatusChange,
|
onlineStatusChange,
|
||||||
isOnlineSelector,
|
isOnlineSelector,
|
||||||
|
userFetchStateSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
|
usernameSelector,
|
||||||
executeGA
|
executeGA
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
|
import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
|
||||||
@ -69,6 +71,7 @@ const metaKeywords = [
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
executeGA: PropTypes.func,
|
executeGA: PropTypes.func,
|
||||||
|
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
|
||||||
fetchUser: PropTypes.func.isRequired,
|
fetchUser: PropTypes.func.isRequired,
|
||||||
flashMessage: PropTypes.shape({
|
flashMessage: PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
@ -82,21 +85,27 @@ const propTypes = {
|
|||||||
pathname: PropTypes.string.isRequired,
|
pathname: PropTypes.string.isRequired,
|
||||||
removeFlashMessage: PropTypes.func.isRequired,
|
removeFlashMessage: PropTypes.func.isRequired,
|
||||||
showFooter: PropTypes.bool,
|
showFooter: PropTypes.bool,
|
||||||
|
signedInUserName: PropTypes.string,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
useTheme: PropTypes.bool
|
useTheme: PropTypes.bool,
|
||||||
|
user: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
flashMessageSelector,
|
flashMessageSelector,
|
||||||
isOnlineSelector,
|
isOnlineSelector,
|
||||||
|
userFetchStateSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
(isSignedIn, flashMessage, isOnline, user) => ({
|
usernameSelector,
|
||||||
|
(isSignedIn, flashMessage, isOnline, fetchState, user) => ({
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
flashMessage,
|
flashMessage,
|
||||||
hasMessage: !!flashMessage.message,
|
hasMessage: !!flashMessage.message,
|
||||||
isOnline,
|
isOnline,
|
||||||
theme: user.theme
|
fetchState,
|
||||||
|
theme: user.theme,
|
||||||
|
user
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -142,14 +151,18 @@ class DefaultLayout extends Component {
|
|||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
hasMessage,
|
hasMessage,
|
||||||
|
fetchState,
|
||||||
flashMessage,
|
flashMessage,
|
||||||
isOnline,
|
isOnline,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
removeFlashMessage,
|
removeFlashMessage,
|
||||||
showFooter = true,
|
showFooter = true,
|
||||||
theme = 'default',
|
theme = 'default',
|
||||||
useTheme = true
|
user,
|
||||||
|
useTheme = true,
|
||||||
|
pathname
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Helmet
|
<Helmet
|
||||||
@ -213,7 +226,7 @@ class DefaultLayout extends Component {
|
|||||||
<style>{fontawesome.dom.css()}</style>
|
<style>{fontawesome.dom.css()}</style>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<WithInstantSearch>
|
<WithInstantSearch>
|
||||||
<Header />
|
<Header fetchState={fetchState} pathName={pathname} user={user} />
|
||||||
<div className={`default-layout`}>
|
<div className={`default-layout`}>
|
||||||
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
||||||
{hasMessage && flashMessage ? (
|
{hasMessage && flashMessage ? (
|
||||||
|
@ -225,7 +225,7 @@ fieldset[disabled] .btn-primary.focus {
|
|||||||
.btn-cta:hover,
|
.btn-cta:hover,
|
||||||
.btn-cta:focus,
|
.btn-cta:focus,
|
||||||
.btn-cta:active:hover {
|
.btn-cta:active:hover {
|
||||||
background-color: #fecc4c;
|
background-color: #fecc4c !important;
|
||||||
border-width: 3px;
|
border-width: 3px;
|
||||||
border-color: #f1a02a;
|
border-color: #f1a02a;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
--blue-light: #99c9ff;
|
--blue-light: #99c9ff;
|
||||||
--blue-dark: #002ead;
|
--blue-dark: #002ead;
|
||||||
--green-light: #acd157;
|
--green-light: #acd157;
|
||||||
|
--green-mid: green;
|
||||||
|
--purple-mid: darkviolet;
|
||||||
--green-dark: #00471b;
|
--green-dark: #00471b;
|
||||||
--red-light: #ffadad;
|
--red-light: #ffadad;
|
||||||
--red-dark: #850000;
|
--red-dark: #850000;
|
||||||
|
@ -21,7 +21,7 @@ exports[`<Profile/> renders correctly 1`] = `
|
|||||||
class="avatar-container col-xs-12"
|
class="avatar-container col-xs-12"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=""
|
class="avatar-container default-border"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="string's avatar"
|
alt="string's avatar"
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
faAward,
|
faAward,
|
||||||
faHeart,
|
faHeart,
|
||||||
faCalendar
|
faCalendar
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import Identicon from 'react-identicons';
|
|
||||||
|
import { AvatarRenderer } from '../../helpers';
|
||||||
|
|
||||||
import SocialIcons from './SocialIcons';
|
import SocialIcons from './SocialIcons';
|
||||||
|
|
||||||
@ -77,29 +78,16 @@ function Camper({
|
|||||||
twitter,
|
twitter,
|
||||||
website
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Col className='avatar-container' xs={12}>
|
<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>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<SocialIcons
|
<SocialIcons
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
.avatar-container .avatar {
|
.avatar-container .avatar {
|
||||||
height: 180px;
|
height: 180px;
|
||||||
|
width: 180px;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
@ -14,6 +16,22 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.supporter-img {
|
.avatar-container div {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container .gold-border {
|
||||||
border: 10px solid var(--yellow-gold);
|
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;
|
||||||
|
}
|
||||||
|
@ -65,7 +65,7 @@ export const LearnPage = ({
|
|||||||
location: { hash = '', state = '' },
|
location: { hash = '', state = '' },
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
fetchState: { pending, complete },
|
fetchState: { pending, complete },
|
||||||
user: { name = '', username = '', completedChallengeCount = 0 },
|
user: { name = '', completedChallengeCount = 0 },
|
||||||
data: {
|
data: {
|
||||||
challengeNode: {
|
challengeNode: {
|
||||||
fields: { slug }
|
fields: { slug }
|
||||||
@ -86,7 +86,6 @@ export const LearnPage = ({
|
|||||||
name={name}
|
name={name}
|
||||||
pending={pending}
|
pending={pending}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
username={username}
|
|
||||||
/>
|
/>
|
||||||
<Map
|
<Map
|
||||||
hash={hashValue}
|
hash={hashValue}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
exports.oldDataVizId = '561add10cb82ac38a17513b3';
|
exports.oldDataVizId = '561add10cb82ac38a17513b3';
|
||||||
|
exports.defaultUserImage = 'https://freecodecamp.com/sample-image.png';
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
/* global cy */
|
/* 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', () => {
|
describe('Navbar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
@ -10,32 +19,6 @@ describe('Navbar', () => {
|
|||||||
cy.get('#universal-nav').should('have.class', 'universal-nav nav-padding');
|
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(
|
it(
|
||||||
'Should take user to learn page when clicked on ' + 'the freeCodeCamp logo',
|
'Should take user to learn page when clicked on ' + 'the freeCodeCamp logo',
|
||||||
() => {
|
() => {
|
||||||
@ -53,7 +36,7 @@ describe('Navbar', () => {
|
|||||||
|
|
||||||
cy.get('.ais-Hits-list')
|
cy.get('.ais-Hits-list')
|
||||||
.children()
|
.children()
|
||||||
.should('have.length', 1);
|
.should('to.have.length.of.at.least', 1);
|
||||||
|
|
||||||
cy.get('.ais-SearchBox').within(() => {
|
cy.get('.ais-SearchBox').within(() => {
|
||||||
cy.get('input').clear();
|
cy.get('input').clear();
|
||||||
@ -61,4 +44,48 @@ describe('Navbar', () => {
|
|||||||
|
|
||||||
cy.get('div.ais-Hits').should('not.be.visible');
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,18 +2,7 @@
|
|||||||
|
|
||||||
describe('The `Update my account settings` button works properly', function() {
|
describe('The `Update my account settings` button works properly', function() {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/');
|
cy.login();
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should take user to their account settings when clicked', function() {
|
it('Should take user to their account settings when clicked', function() {
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,3 +1,4 @@
|
|||||||
|
/* global cy Cypress*/
|
||||||
// ***********************************************
|
// ***********************************************
|
||||||
// This example commands.js shows you how to
|
// This example commands.js shows you how to
|
||||||
// create various custom commands and overwrite
|
// create various custom commands and overwrite
|
||||||
@ -31,3 +32,8 @@
|
|||||||
//
|
//
|
||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => {});
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => {});
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.contains("Get started (it's free)").click({ force: true });
|
||||||
|
});
|
||||||
|
@ -7,6 +7,8 @@ const debug = require('debug');
|
|||||||
const log = debug('fcc:tools:seedLocalAuthUser');
|
const log = debug('fcc:tools:seedLocalAuthUser');
|
||||||
const { MONGOHQ_URL } = process.env;
|
const { MONGOHQ_URL } = process.env;
|
||||||
|
|
||||||
|
const defaulUserImage = require('../../../config/misc').defaulUserImage;
|
||||||
|
|
||||||
function handleError(err, client) {
|
function handleError(err, client) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Oh noes!! Error seeding local auth user.');
|
console.error('Oh noes!! Error seeding local auth user.');
|
||||||
@ -48,7 +50,7 @@ MongoClient.connect(MONGOHQ_URL, { useNewUrlParser: true }, function(
|
|||||||
about: '',
|
about: '',
|
||||||
name: 'Development User',
|
name: 'Development User',
|
||||||
location: '',
|
location: '',
|
||||||
picture: 'https://github.com/identicons/camperbot.png',
|
picture: defaulUserImage,
|
||||||
acceptedPrivacyTerms: true,
|
acceptedPrivacyTerms: true,
|
||||||
sendQuincyEmail: false,
|
sendQuincyEmail: false,
|
||||||
currentChallengeId: '',
|
currentChallengeId: '',
|
||||||
|
Reference in New Issue
Block a user