feat(client): redesigned navigation (#40709)

Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
Nicholas Carrigan (he/him)
2021-01-27 12:34:29 -08:00
committed by Mrugesh Mohapatra
parent 9014fff6c4
commit 58c6c54c67
17 changed files with 470 additions and 288 deletions

View File

@ -55,7 +55,10 @@
"sign-out": "退出", "sign-out": "退出",
"curriculum": "课程", "curriculum": "课程",
"forum": "论坛", "forum": "论坛",
"radio": "Radio",
"profile": "个人资料", "profile": "个人资料",
"news": "News",
"donate": "Donate",
"update-settings": "更新我的账号设置", "update-settings": "更新我的账号设置",
"sign-me-out": "退出登录 freeCodeCamp", "sign-me-out": "退出登录 freeCodeCamp",
"flag-user": "标记该用户的账号为滥用", "flag-user": "标记该用户的账号为滥用",
@ -314,6 +317,7 @@
"donate": { "donate": {
"title": "支持我们的非营利组织", "title": "支持我们的非营利组织",
"processing": "我们正在处理你的捐款。", "processing": "我们正在处理你的捐款。",
"thanks": "Thanks for donating",
"thank-you": "谢谢你成为我们的支持者。", "thank-you": "谢谢你成为我们的支持者。",
"thank-you-2": "谢谢你成为 freeCodeCamp 的支持者。现在你已设置定期捐款。", "thank-you-2": "谢谢你成为 freeCodeCamp 的支持者。现在你已设置定期捐款。",
"additional": "你可以使用这个链接 <0>{{url}}</0> 额外进行一次性捐款:", "additional": "你可以使用这个链接 <0>{{url}}</0> 额外进行一次性捐款:",

View File

@ -57,7 +57,10 @@
"sign-out": "Sign out", "sign-out": "Sign out",
"curriculum": "Curriculum", "curriculum": "Curriculum",
"forum": "Forum", "forum": "Forum",
"radio": "Radio",
"profile": "Profile", "profile": "Profile",
"news": "News",
"donate": "Donate",
"update-settings": "Update my account settings", "update-settings": "Update my account settings",
"sign-me-out": "Sign me out of freeCodeCamp", "sign-me-out": "Sign me out of freeCodeCamp",
"flag-user": "Flag This User's Account for Abuse", "flag-user": "Flag This User's Account for Abuse",
@ -316,6 +319,7 @@
"donate": { "donate": {
"title": "Support our nonprofit", "title": "Support our nonprofit",
"processing": "We are processing your donation.", "processing": "We are processing your donation.",
"thanks": "Thanks for donating",
"thank-you": "Thank you for being a supporter.", "thank-you": "Thank you for being a supporter.",
"thank-you-2": "Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.", "thank-you-2": "Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.",
"additional": "You can make an additional one-time donation of any amount using this link: <0>{{url}}</0>", "additional": "You can make an additional one-time donation of any amount using this link: <0>{{url}}</0>",
@ -376,7 +380,7 @@
}, },
"search": { "search": {
"label": "Search", "label": "Search",
"placeholder": "Search 6,000+ tutorial", "placeholder": "Search 6,000+ tutorials",
"see-results": "See all results for {{searchQuery}}", "see-results": "See all results for {{searchQuery}}",
"no-tutorials": "No tutorials found", "no-tutorials": "No tutorials found",
"try": "Looking for something? Try the search bar on this page.", "try": "Looking for something? Try the search bar on this page.",

View File

@ -57,7 +57,10 @@
"sign-out": "Cerrar sesión", "sign-out": "Cerrar sesión",
"curriculum": "Plan de estudio", "curriculum": "Plan de estudio",
"forum": "Foro", "forum": "Foro",
"radio": "Radio",
"profile": "Perfil", "profile": "Perfil",
"news": "News",
"donate": "Donate",
"update-settings": "Actualizar la configuración de mi cuenta", "update-settings": "Actualizar la configuración de mi cuenta",
"sign-me-out": "Cerrar sesión en freeCodeCamp", "sign-me-out": "Cerrar sesión en freeCodeCamp",
"flag-user": "Marcar la cuenta de este usuario por abuso", "flag-user": "Marcar la cuenta de este usuario por abuso",
@ -316,6 +319,7 @@
"donate": { "donate": {
"title": "Apoya a nuestra organización sin fines de lucro", "title": "Apoya a nuestra organización sin fines de lucro",
"processing": "Estamos procesando tu donación.", "processing": "Estamos procesando tu donación.",
"thanks": "Thanks for donating",
"thank-you": "Gracias por tu apoyo.", "thank-you": "Gracias por tu apoyo.",
"thank-you-2": "Gracias por apoyar a freeCodeCamp. Actualmente tienes una donación recurrente.", "thank-you-2": "Gracias por apoyar a freeCodeCamp. Actualmente tienes una donación recurrente.",
"additional": "Puede hacer una donación adicional por única vez de cualquier monto utilizando este enlace: <0>{{url}}</0>", "additional": "Puede hacer una donación adicional por única vez de cualquier monto utilizando este enlace: <0>{{url}}</0>",

View File

@ -61,7 +61,10 @@ const translationsSchema = {
'sign-out': 'Sign out', 'sign-out': 'Sign out',
curriculum: 'Curriculum', curriculum: 'Curriculum',
forum: 'Forum', forum: 'Forum',
radio: 'Radio',
profile: 'Profile', profile: 'Profile',
news: 'News',
donate: 'Donate',
'update-settings': 'Update my account settings', 'update-settings': 'Update my account settings',
'sign-me-out': 'Sign me out of freeCodeCamp', 'sign-me-out': 'Sign me out of freeCodeCamp',
'flag-user': "Flag This User's Account for Abuse", 'flag-user': "Flag This User's Account for Abuse",
@ -365,6 +368,7 @@ const translationsSchema = {
donate: { donate: {
title: 'Support our nonprofit', title: 'Support our nonprofit',
processing: 'We are processing your donation.', processing: 'We are processing your donation.',
thanks: 'Thanks for donating',
'thank-you': 'Thank you for being a supporter.', 'thank-you': 'Thank you for being a supporter.',
'thank-you-2': 'thank-you-2':
'Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.', 'Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.',

View File

@ -1,13 +1,13 @@
/* 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 renderer from 'react-test-renderer';
/* import { useTranslation } from 'react-i18next'; /* import { useTranslation } from 'react-i18next';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import i18n from '../../../i18n/configForTests';*/ import i18n from '../../../i18n/configForTests';*/
import { UniversalNav } from './components/UniversalNav'; import { UniversalNav } from './components/UniversalNav';
import { AuthOrProfile } from './components/NavLinks'; import { NavLinks } from './components/NavLinks';
import AuthOrProfile from './components/AuthOrProfile';
describe('<UniversalNav />', () => { describe('<UniversalNav />', () => {
const UniversalNavProps = { const UniversalNavProps = {
@ -26,32 +26,48 @@ describe('<UniversalNav />', () => {
}); });
describe('<NavLinks />', () => { describe('<NavLinks />', () => {
it('shows Curriculum and Sign In buttons when not signed in', () => { it('has expected navigation links', () => {
const landingPageProps = { const landingPageProps = {
fetchState: {
pending: false pending: false
},
user: {
isUserDonating: false,
username: '',
theme: 'default'
},
i18n: {
language: 'en'
},
toggleNightMode: theme => theme
}; };
const shallow = new ShallowRenderer(); const shallow = new ShallowRenderer();
shallow.render(<AuthOrProfile {...landingPageProps} />); shallow.render(<NavLinks {...landingPageProps} />);
const result = shallow.getRenderOutput(); const result = shallow.getRenderOutput();
expect( expect(
hasRadioNavItem(result) &&
hasForumNavItem(result) && hasForumNavItem(result) &&
hasCurriculumNavItem(result) && hasCurriculumNavItem(result) &&
hasSignInButton(result) hasNewsNavItem(result) &&
hasDonateNavItem(result)
).toBeTruthy(); ).toBeTruthy();
}); });
});
describe('<AuthOrProfile />', () => {
it('has avatar with default border for default users', () => { it('has avatar with default border for default users', () => {
const defaultUserProps = { const defaultUserProps = {
user: { user: {
username: 'test-user', username: 'test-user',
picture: 'https://freecodecamp.org/image.png' picture: 'https://freecodecamp.org/image.png'
}, },
pending: false pending: false,
pathName: '/learn'
}; };
const componentTree = renderer const shallow = new ShallowRenderer();
.create(<AuthOrProfile {...defaultUserProps} />) shallow.render(<AuthOrProfile {...defaultUserProps} />);
.toJSON(); const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy(); expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy();
}); });
@ -63,11 +79,12 @@ describe('<NavLinks />', () => {
picture: 'https://freecodecamp.org/image.png', picture: 'https://freecodecamp.org/image.png',
isDonating: true isDonating: true
}, },
pending: false pending: false,
pathName: '/learn'
}; };
const componentTree = renderer const shallow = new ShallowRenderer();
.create(<AuthOrProfile {...donatingUserProps} />) shallow.render(<AuthOrProfile {...donatingUserProps} />);
.toJSON(); const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'gold-border')).toBeTruthy(); expect(avatarHasClass(componentTree, 'gold-border')).toBeTruthy();
}); });
@ -79,12 +96,13 @@ describe('<NavLinks />', () => {
picture: 'https://freecodecamp.org/image.png', picture: 'https://freecodecamp.org/image.png',
yearsTopContributor: [2020] yearsTopContributor: [2020]
}, },
pending: false pending: false,
pathName: '/learn'
}; };
const componentTree = renderer const shallow = new ShallowRenderer();
.create(<AuthOrProfile {...topContributorUserProps} />) shallow.render(<AuthOrProfile {...topContributorUserProps} />);
.toJSON(); const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'blue-border')).toBeTruthy(); expect(avatarHasClass(componentTree, 'blue-border')).toBeTruthy();
}); });
@ -96,41 +114,60 @@ describe('<NavLinks />', () => {
isDonating: true, isDonating: true,
yearsTopContributor: [2020] yearsTopContributor: [2020]
}, },
pending: false pending: false,
pathName: '/learn'
}; };
const componentTree = renderer const shallow = new ShallowRenderer();
.create(<AuthOrProfile {...topDonatingContributorUserProps} />) shallow.render(<AuthOrProfile {...topDonatingContributorUserProps} />);
.toJSON(); const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'purple-border')).toBeTruthy(); expect(avatarHasClass(componentTree, 'purple-border')).toBeTruthy();
}); });
}); });
const navigationLinks = (component, navItem) => { const navigationLinks = (component, navItem) => {
return component.props.children[0].props.children[navItem].props.children return component.props.children.props.children[navItem].props.children.props;
.props;
}; };
const profileNavItem = component => component[2].children[0]; const profileNavItem = component => component.props.children;
const hasDonateNavItem = component => {
const { children, to } = navigationLinks(component, 0);
return children === 'buttons.donate' && to === '/donate';
};
const hasForumNavItem = component => { const hasForumNavItem = component => {
const { children, to } = navigationLinks(component, 0); const { children, to } = navigationLinks(component, 1);
return ( return (
children === 'buttons.forum' && to === 'https://forum.freecodecamp.org' children === 'buttons.forum' && to === 'https://forum.freecodecamp.org'
); );
}; };
const hasNewsNavItem = component => {
const { children, to } = navigationLinks(component, 2);
return (
children === 'buttons.news' && to === 'https://www.freecodecamp.org/news'
);
};
const hasCurriculumNavItem = component => { const hasCurriculumNavItem = component => {
const { children, to } = navigationLinks(component, 1); const { children, to } = navigationLinks(component, 3);
return children === 'buttons.curriculum' && to === '/learn'; return children === 'buttons.curriculum' && to === '/learn';
}; };
const hasSignInButton = component => const hasRadioNavItem = component => {
component.props.children[1].props.children === 'buttons.sign-in'; const { children, to } = navigationLinks(component, 5);
const avatarHasClass = (componentTree, classes) => {
// componentTree[1].children[0].children[1].props.className
return ( return (
profileNavItem(componentTree).children[1].props.className === children === 'buttons.radio' && to === 'https://coderadio.freecodecamp.org'
'avatar-container ' + classes );
};
/* TODO: Apply this to Universalnav component
const hasSignInButton = component =>
component.props.children[1].props.children === 'buttons.sign-in';
*/
const avatarHasClass = (componentTree, classes) => {
return (
profileNavItem(componentTree).props.className ===
'avatar-nav-link ' + classes
); );
}; };

View File

@ -39,15 +39,6 @@ export function AuthOrProfile({ user, pathName, pending }) {
} else { } else {
return ( return (
<> <>
<li>
<Link className='nav-link' to='/learn'>
{t('buttons.curriculum')}
</Link>
</li>
<li>
<Link className='nav-link' to={`/${user.username}`}>
{t('buttons.profile')}
</Link>
<Link <Link
className={`avatar-nav-link ${badgeColorClass}`} className={`avatar-nav-link ${badgeColorClass}`}
to={`/${user.username}`} to={`/${user.username}`}
@ -58,7 +49,6 @@ export function AuthOrProfile({ user, pathName, pending }) {
userName={user.username} userName={user.username}
/> />
</Link> </Link>
</li>
</> </>
); );
} }

View File

@ -1,21 +1,28 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import AuthOrProfile from './AuthOrProfile';
const MenuButton = props => { const MenuButton = props => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<>
<button <button
aria-expanded={props.displayMenu} aria-expanded={props.displayMenu}
className={ className={
'toggle-button-nav' + (props.displayMenu ? ' reverse-toggle-color' : '') 'toggle-button-nav' +
(props.displayMenu ? ' reverse-toggle-color' : '')
} }
onClick={props.onClick} onClick={props.onClick}
ref={props.innerRef} ref={props.innerRef}
> >
{t('buttons.menu')} {t('buttons.menu')}
</button> </button>
<span className='navatar'>
<AuthOrProfile user={props.user} />
</span>
</>
); );
}; };
@ -24,7 +31,8 @@ MenuButton.propTypes = {
className: PropTypes.string, className: PropTypes.string,
displayMenu: PropTypes.bool.isRequired, displayMenu: PropTypes.bool.isRequired,
innerRef: PropTypes.object, innerRef: PropTypes.object,
onClick: PropTypes.func.isRequired onClick: PropTypes.func.isRequired,
user: PropTypes.object
}; };
export default MenuButton; export default MenuButton;

View File

@ -1,89 +1,154 @@
import React from 'react'; import React, { Component } from 'react';
import { Link, SkeletonSprite, AvatarRenderer } from '../../helpers'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Login from '../components/Login'; import { withTranslation } from 'react-i18next';
import { forumLocation } from '../../../../../config/env.json'; import { Link, SkeletonSprite } from '../../helpers';
import { useTranslation } from 'react-i18next'; import { updateUserFlag } from '../../../redux/settings';
import {
forumLocation,
radioLocation,
newsLocation
} from '../../../../../config/env.json';
import createLanguageRedirect from '../../createLanguageRedirect';
const {
availableLangs,
i18nextCodes,
langDisplayNames
} = require('../../../../i18n/allLangs');
const locales = availableLangs.client;
const propTypes = { const propTypes = {
displayMenu: PropTypes.bool, displayMenu: PropTypes.bool,
fetchState: PropTypes.shape({ pending: PropTypes.bool }), fetchState: PropTypes.shape({ pending: PropTypes.bool }),
i18n: PropTypes.object,
t: PropTypes.func,
toggleNightMode: PropTypes.func.isRequired,
user: PropTypes.object user: PropTypes.object
}; };
export function AuthOrProfile({ user, pending }) { const mapDispatchToProps = {
const { t } = useTranslation(); toggleNightMode: theme => updateUserFlag({ theme })
const isUserDonating = user && user.isDonating; };
const isUserSignedIn = user && user.username;
const isTopContributor =
user && user.yearsTopContributor && user.yearsTopContributor.length > 0;
const CurriculumAndForumLinks = ( export class NavLinks extends Component {
<> toggleTheme(currentTheme = 'default', toggleNightMode) {
<li> console.log('attempting to toggle night mode');
toggleNightMode(currentTheme === 'night' ? 'default' : 'night');
}
render() {
const {
displayMenu,
fetchState,
i18n,
t,
toggleNightMode,
user: { isUserDonating = false, username, theme }
} = this.props;
const { pending } = fetchState;
return pending ? (
<div className='nav-skeleton'>
<SkeletonSprite />
</div>
) : (
<div className='main-nav-group'>
<ul className={'nav-list' + (displayMenu ? ' display-menu' : '')}>
<li key='donate'>
{isUserDonating ? (
<span className='nav-link'>{t('donate.thanks')}</span>
) : (
<Link <Link
className='nav-link' className='nav-link'
external={true} external={true}
sameTab={true} sameTab={false}
to='/donate'
>
{t('buttons.donate')}
</Link>
)}
</li>
<li key='forum'>
<Link
className='nav-link'
external={true}
sameTab={false}
to={forumLocation} to={forumLocation}
> >
{t('buttons.forum')} {t('buttons.forum')}
</Link> </Link>
</li> </li>
<li> <li key='news'>
<Link
className='nav-link'
external={true}
sameTab={false}
to={newsLocation}
>
{t('buttons.news')}
</Link>
</li>
<li key='learn'>
<Link className='nav-link' to='/learn'> <Link className='nav-link' to='/learn'>
{t('buttons.curriculum')} {t('buttons.curriculum')}
</Link> </Link>
</li> </li>
</> {username && (
); <li key='profile'>
<Link className='nav-link' to={`/${username}`}>
if (pending) {
return (
<div className='nav-skeleton'>
<SkeletonSprite />
</div>
);
} else if (!isUserSignedIn) {
return (
<>
{CurriculumAndForumLinks}
<Login data-test-label='landing-small-cta'>
{t('buttons.sign-in')}
</Login>
</>
);
} else {
return (
<>
{CurriculumAndForumLinks}
<li>
<Link className='nav-link' to={`/${user.username}`}>
{t('buttons.profile')} {t('buttons.profile')}
<AvatarRenderer
isDonating={isUserDonating}
isTopContributor={isTopContributor}
picture={user.picture}
userName={user.username}
/>
</Link> </Link>
</li> </li>
</> )}
); <li key='radio'>
} <Link
} className='nav-link'
external={true}
export function NavLinks({ displayMenu, user, fetchState }) { sameTab={false}
const { pending } = fetchState; to={radioLocation}
return ( >
<div className='main-nav-group'> {t('buttons.radio')}
<ul className={'nav-list' + (displayMenu ? ' display-flex' : '')}> </Link>
<AuthOrProfile pending={pending} user={user} /> </li>
<li key='theme'>
<button
className='nav-link'
disabled={!username}
onClick={() => this.toggleTheme(theme, toggleNightMode)}
>
{username
? t('settings.labels.night-mode') +
(theme === 'night' ? ' ✓' : '')
: 'Sign in to change theme'}
</button>
</li>
<li key='lang-header'>
<span className='nav-link'>{t('footer.language')}</span>
</li>
{locales.map(lang => (
<li key={'lang-' + lang}>
<Link
className='nav-link sub-link'
to={createLanguageRedirect(lang)}
>
{langDisplayNames[lang]}
{i18n.language === i18nextCodes[lang] ? ' ✓' : ''}
</Link>
</li>
))}
</ul> </ul>
</div> </div>
); );
} }
}
NavLinks.propTypes = propTypes; NavLinks.propTypes = propTypes;
NavLinks.displayName = 'NavLinks'; NavLinks.displayName = 'NavLinks';
export default NavLinks;
export default connect(
null,
mapDispatchToProps
)(withTranslation()(NavLinks));

View File

@ -17,11 +17,11 @@ export const UniversalNav = ({
fetchState fetchState
}) => ( }) => (
<nav <nav
className={'universal-nav nav-padding' + (displayMenu ? ' expand-nav' : '')} className={'universal-nav' + (displayMenu ? ' expand-nav' : '')}
id='universal-nav' id='universal-nav'
> >
<div <div
className={'universal-nav-left' + (displayMenu ? ' display-flex' : '')} className={'universal-nav-left' + (displayMenu ? ' display-search' : '')}
> >
<SearchBar innerRef={searchBarRef} /> <SearchBar innerRef={searchBarRef} />
</div> </div>
@ -32,13 +32,14 @@ export const UniversalNav = ({
</Link> </Link>
</div> </div>
<div className='universal-nav-right main-nav'> <div className='universal-nav-right main-nav'>
<NavLinks displayMenu={displayMenu} fetchState={fetchState} user={user} />
</div>
<MenuButton <MenuButton
displayMenu={displayMenu} displayMenu={displayMenu}
innerRef={menuButtonRef} innerRef={menuButtonRef}
onClick={toggleDisplayMenu} onClick={toggleDisplayMenu}
user={user}
/> />
</div>
<NavLinks displayMenu={displayMenu} fetchState={fetchState} user={user} />
</nav> </nav>
); );

View File

@ -3,10 +3,9 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
/* overflow-y: hidden; */
height: 40px; height: 40px;
font-size: 18px; font-size: 18px;
font-family: 'Lato', sans-serif; font-family: 'Roboto-Mono', sans-serif;
height: var(--header-height); height: var(--header-height);
background: var(--theme-color); background: var(--theme-color);
position: fixed; position: fixed;
@ -25,22 +24,37 @@
font-family: 'Roboto Mono', monospace; font-family: 'Roboto Mono', monospace;
} }
.universal-nav-left {
display: flex;
flex: 1 0 33%;
margin-left: 0px;
z-index: 2000;
}
.universal-nav-middle { .universal-nav-middle {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1 0 33%;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
margin-right: 10px;
letter-spacing: 0.4px; letter-spacing: 0.4px;
white-space: nowrap; white-space: nowrap;
-ms-overflow-scrolling: touch; -ms-overflow-scrolling: touch;
} }
.universal-nav-right {
display: flex;
justify-content: flex-end;
align-items: center;
flex: 1 0 33%;
height: var(--header-height);
}
#universal-nav-logo { #universal-nav-logo {
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;
margin: 0px; margin: 0 auto;
color: var(--gray-00); color: var(--gray-00);
font-size: 1.7rem; font-size: 1.7rem;
line-height: 1em; line-height: 1em;
@ -64,15 +78,19 @@
} }
.nav-list { .nav-list {
display: flex; display: none;
position: absolute;
background-color: var(--theme-color);
top: calc(var(--header-height) * 2);
right: 0px;
flex-wrap: wrap;
justify-content: flex-end;
width: 100vw;
height: auto;
margin: 0 0 0 -12px; margin: 0 0 0 -12px;
padding: 0; padding: 0;
list-style: none; list-style: none;
align-items: center; max-width: 300px;
}
.nav-list {
height: var(--header-height);
} }
.nav-list li { .nav-list li {
@ -81,32 +99,41 @@
padding: 0; padding: 0;
} }
.nav-list li a.nav-link { .nav-link {
display: flex; display: flex;
margin: 0; align-items: center;
padding: 0 15px; padding: 2px 15px 0 25px;
color: var(--gray-00); color: var(--gray-00);
background-color: var(--gray-90);
opacity: 1; opacity: 1;
white-space: nowrap; white-space: nowrap;
height: var(--header-height); height: var(--header-height);
width: 100%;
align-items: center; align-items: center;
border: none;
} }
.nav-list li:last-child { .nav-list span.nav-link {
display: flex; padding: 2px 15px 0 15px;
flex-direction: row; background-color: var(--gray-75);
} }
.nav-list li:last-child a { .nav-list a.nav-link:hover,
padding-right: 15px; .nav-list button.nav-link:hover {
}
.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 button:disabled {
background-color: var(--gray-75);
}
.nav-list button:disabled:hover {
color: white;
background-color: var(--gray-75);
}
.nav-list li a.nav-link:focus { .nav-list li a.nav-link:focus {
background: var(--theme-color); background: var(--theme-color);
color: white; color: white;
@ -136,15 +163,7 @@
margin-right: 25px; margin-right: 25px;
} }
.universal-nav-right {
flex-shrink: 0;
display: flex;
align-items: center;
height: var(--header-height);
}
.toggle-button-nav { .toggle-button-nav {
display: none;
padding: 2px 14px 2px; padding: 2px 14px 2px;
border: 1px solid var(--gray-00); border: 1px solid var(--gray-00);
font-family: 'lato', sans-serif; font-family: 'lato', sans-serif;
@ -153,8 +172,8 @@
outline: none; outline: none;
background-color: var(--theme-color); background-color: var(--theme-color);
cursor: pointer; cursor: pointer;
margin-top: 4px; max-height: calc(var(--header-height) - 6px);
height: auto; margin-right: 10px;
} }
.toggle-button-nav:hover { .toggle-button-nav:hover {
@ -163,88 +182,61 @@
border: 1px solid var(--gray-00); border: 1px solid var(--gray-00);
} }
.nav-list li .avatar-container { .navatar {
display: block; display: contents;
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 { .navatar .avatar-nav-link {
display: inline-block; height: 31px;
width: 31px;
}
.navatar .default-border {
border: none;
}
.navatar .avatar-container svg {
display: inline;
background: var(--secondary-background); background: var(--secondary-background);
} }
.nav-list .avatar-container svg,
.nav-list .avatar-container img { .navatar .avatar-container svg,
.navatar .avatar-container img {
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.nav-list .gold-border { .gold-border {
border: 2px solid var(--yellow-gold); border: 2px solid var(--yellow-gold);
} }
.nav-list .blue-border { .blue-border {
border: 2px solid var(--blue-mid); border: 2px solid var(--blue-mid);
} }
.nav-list .purple-border { .purple-border {
border: 2px solid var(--purple-mid); border: 2px solid var(--purple-mid);
} }
.nav-list .default-border { .default-border {
border: 2px solid transparent; border: 2px solid transparent;
} }
@media (max-width: 300px) { .expand-nav {
.nav-list li a.nav-link { height: var(--header-height);
width: 50vw;
text-align: center;
}
} }
@media (max-width: 1079px) { .display-menu {
.site-header { display: inherit;
padding-right: 0; text-align: left;
padding-left: 0; margin-top: calc(-1 * var(--header-height));
}
.universal-nav-middle {
margin-right: 0;
}
.universal-nav-left {
display: none;
}
.nav-list {
display: none;
position: absolute;
background-color: var(--theme-color);
top: calc(var(--header-height) * 2);
right: 0px;
flex-wrap: wrap;
justify-content: flex-end;
width: 100vw;
height: auto;
}
.display-flex {
display: flex;
} }
.toggle-button-nav { .toggle-button-nav {
display: flex; display: flex;
} }
.expand-nav {
min-height: calc(3 * var(--header-height));
}
.reverse-toggle-color { .reverse-toggle-color {
background-color: var(--gray-00); background-color: var(--gray-00);
color: var(--theme-color); color: var(--theme-color);
@ -255,15 +247,31 @@
color: var(--theme-color); color: var(--theme-color);
} }
.universal-nav-left form { @media (max-width: 980px) {
.universal-nav-left {
display: none; display: none;
} }
.universal-nav-middle {
flex: none;
}
.display-search {
display: initial;
}
.universal-nav-right {
flex: 1 0 50%;
display: flex;
justify-content: flex-end;
}
.fcc_searchBar .ais-SearchBox-form { .fcc_searchBar .ais-SearchBox-form {
display: flex; display: flex;
position: absolute; position: absolute;
top: var(--header-height); top: var(--header-height);
left: 15px; left: 15px;
max-width: calc(100vw - 350px);
} }
#universal-nav-logo { #universal-nav-logo {
@ -272,40 +280,49 @@
left: 17px; left: 17px;
top: 0px; top: 0px;
} }
.expand-nav {
min-height: calc(2 * var(--header-height));
}
} }
@media (min-width: 1080px) { @media (max-width: 600px) {
.universal-nav-middle { .nav-list {
flex: 1 0 30%; min-width: 100%;
margin-right: 0px; top: calc(var(--header-height) * 2);
margin-top: 0;
} }
.universal-nav-left {
display: flex; .fcc_searchBar .ais-SearchBox-form {
flex: 1 0 35%; max-width: 100%;
margin-left: 0px;
} }
.ais-SearchBox-input {
min-width: 100%;
}
.ais-Hits {
min-width: calc(100% - 30px);
}
}
@media (max-width: 400px) {
.universal-nav {
padding: 0 5px;
}
.universal-nav {
padding: 0 5px;
}
#universal-nav-logo { #universal-nav-logo {
margin-left: auto; left: 5px;
margin-right: auto; max-width: 45%;
}
.universal-nav-right {
flex: 1 0 35%;
margin-left: auto;
}
.main-nav-group {
margin-left: auto;
} }
} }
@media (min-width: 1500px) { .signup-btn {
.universal-nav-middle { max-height: calc(var(--header-height) - 6px);
flex: 1 0 33%; padding: 0 4px;
} margin-left: 2px;
.universal-nav-left {
flex: 1 0 33%;
}
.universal-nav-right {
flex: 1 0 33%;
}
} }

View File

@ -0,0 +1,10 @@
import { homeLocation, chineseHome } from '../../../config/env.json';
const createLanguageRedirect = lang => {
const path = window.location.pathname;
if (lang === 'chinese') return `${chineseHome}${path}`;
if (lang === 'english') return `${homeLocation}${path}`;
return `${homeLocation}/${lang}${path}`;
};
export default createLanguageRedirect;

View File

@ -14,8 +14,8 @@
--purple-dark: #5a01a7; --purple-dark: #5a01a7;
--yellow-light: #ffc300; --yellow-light: #ffc300;
--yellow-dark: #4d3800; --yellow-dark: #4d3800;
--blue-light: 153, 201, 255; --blue-light: rgb(153, 201, 255);
--blue-dark: 0, 46, 173; --blue-dark: rgb(0, 46, 173);
--green-light: #acd157; --green-light: #acd157;
--blue-mid: #198eee; --blue-mid: #198eee;
--purple-mid: darkviolet; --purple-mid: darkviolet;

View File

@ -36,7 +36,7 @@
.fcc_searchBar .ais-Hits { .fcc_searchBar .ais-Hits {
top: 70px; top: 70px;
width: calc(100vw - 30px); width: calc(100vw - 350px);
left: 15px; left: 15px;
} }
@ -126,7 +126,7 @@ and arrow keys */
} }
.ais-SearchBox-input { .ais-SearchBox-input {
width: calc(100vw - 30px); width: calc(100vw - 350px);
} }
.fcc_searchBar .ais-SearchBox-form { .fcc_searchBar .ais-SearchBox-form {
@ -136,7 +136,7 @@ and arrow keys */
right: 15px; right: 15px;
} }
@media (min-width: 1080px) { @media (min-width: 980px) {
.ais-SearchBox-input { .ais-SearchBox-input {
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;

View File

@ -7,7 +7,7 @@ const themeKey = 'fcc-theme';
const defaultTheme = 'default'; const defaultTheme = 'default';
const nightTheme = 'night'; const nightTheme = 'night';
function setTheme(currentTheme = defaultTheme, theme) { export function setTheme(currentTheme = defaultTheme, theme) {
if (currentTheme !== theme) { if (currentTheme !== theme) {
store.set(themeKey, theme); store.set(themeKey, theme);
} }

View File

@ -15,6 +15,7 @@ const {
API_LOCATION: apiLocation, API_LOCATION: apiLocation,
FORUM_LOCATION: forumLocation, FORUM_LOCATION: forumLocation,
NEWS_LOCATION: newsLocation, NEWS_LOCATION: newsLocation,
RADIO_LOCATION: radioLocation,
CLIENT_LOCALE: clientLocale, CLIENT_LOCALE: clientLocale,
CURRICULUM_LOCALE: curriculumLocale, CURRICULUM_LOCALE: curriculumLocale,
SHOW_LOCALE_DROPDOWN_MENU: showLocaleDropdownMenu, SHOW_LOCALE_DROPDOWN_MENU: showLocaleDropdownMenu,
@ -23,14 +24,17 @@ const {
ALGOLIA_API_KEY: algoliaAPIKey, ALGOLIA_API_KEY: algoliaAPIKey,
PAYPAL_CLIENT_ID: paypalClientId, PAYPAL_CLIENT_ID: paypalClientId,
DEPLOYMENT_ENV: deploymentEnv, DEPLOYMENT_ENV: deploymentEnv,
SHOW_UPCOMING_CHANGES: showUpcomingChanges SHOW_UPCOMING_CHANGES: showUpcomingChanges,
CHINESE_HOME: chineseHome
} = process.env; } = process.env;
const locations = { const locations = {
homeLocation, homeLocation,
apiLocation, apiLocation,
forumLocation, forumLocation,
newsLocation newsLocation,
radioLocation,
chineseHome
}; };
module.exports = Object.assign(locations, { module.exports = Object.assign(locations, {

View File

@ -5,18 +5,46 @@ const selectors = {
smallCallToAction: "[data-test-label='landing-small-cta']", smallCallToAction: "[data-test-label='landing-small-cta']",
navigationLinks: '.nav-list', navigationLinks: '.nav-list',
avatarContainer: '.avatar-container', avatarContainer: '.avatar-container',
defaultAvatar: '.avatar-container svg' defaultAvatar: '.avatar-container',
menuButton: '.toggle-button-nav'
}; };
let appHasStarted;
function spyOnListener(win) {
const addListener = win.EventTarget.prototype.addEventListener;
win.EventTarget.prototype.addEventListener = function(name) {
if (name === 'click') {
appHasStarted = true;
win.EventTarget.prototype.addEventListener = addListener;
}
return addListener.apply(this, arguments);
};
}
function waitForAppStart() {
return new Promise(resolve => {
const isReady = () => {
if (appHasStarted) {
return resolve();
}
return setTimeout(isReady, 0);
};
isReady();
});
}
describe('Navbar', () => { describe('Navbar', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/'); appHasStarted = false;
cy.viewport(1200, 660); cy.visit('/', {
onBeforeLoad: spyOnListener
}).then(waitForAppStart);
cy.viewport(1300, 660);
}); });
it('Should render properly', () => { it('Should render properly', () => {
cy.get('#universal-nav').should('be.visible'); cy.get('#universal-nav').should('be.visible');
cy.get('#universal-nav').should('have.class', 'universal-nav nav-padding'); cy.get('#universal-nav').should('have.class', 'universal-nav');
}); });
it( it(
@ -51,9 +79,10 @@ describe('Navbar', () => {
// have the curriculum and CTA on landing and /learn pages. // have the curriculum and CTA on landing and /learn pages.
it( it(
'Should have `Forum` and `Curriculum` links on landing and learn pages' + 'Should have `Radio`, `Forum`, and `Curriculum` links on landing and learn pages' +
'page when not signed in', 'page when not signed in',
() => { () => {
cy.get(selectors.menuButton).click();
cy.get(selectors.navigationLinks).contains('Forum'); cy.get(selectors.navigationLinks).contains('Forum');
cy.get(selectors.navigationLinks) cy.get(selectors.navigationLinks)
.contains('Curriculum') .contains('Curriculum')
@ -61,14 +90,16 @@ describe('Navbar', () => {
cy.url().should('include', '/learn'); cy.url().should('include', '/learn');
cy.get(selectors.navigationLinks).contains('Curriculum'); cy.get(selectors.navigationLinks).contains('Curriculum');
cy.get(selectors.navigationLinks).contains('Forum'); cy.get(selectors.navigationLinks).contains('Forum');
cy.get(selectors.navigationLinks).contains('Radio');
} }
); );
it( it(
'Should have `Sign in` link on landing and learn pages' + 'Should have `Sign in` link on landing and learn pages' +
'page when not signed in', ' when not signed in',
() => { () => {
cy.contains(selectors.smallCallToAction, 'Sign in'); cy.contains(selectors.smallCallToAction, 'Sign in');
cy.get(selectors.menuButton).click();
cy.get(selectors.navigationLinks) cy.get(selectors.navigationLinks)
.contains('Curriculum') .contains('Curriculum')
.click(); .click();
@ -77,17 +108,18 @@ describe('Navbar', () => {
); );
it('Should have `Profile` link when user is signed in', () => { it('Should have `Profile` link when user is signed in', () => {
cy.login() cy.login();
.get(selectors.navigationLinks) cy.get('a[href*="/settings"]').should('be.visible');
cy.get(selectors.menuButton).click();
cy.get(selectors.navigationLinks)
.contains('Profile') .contains('Profile')
.click(); .click();
cy.url().should('include', '/developmentuser'); cy.url().should('include', '/developmentuser');
}); });
it('Should have a profile image with class `default-border`', () => { it('Should have a profile image with class `default-border`', () => {
cy.login() cy.login();
.get(selectors.avatarContainer) cy.get(selectors.avatarContainer).should('have.class', 'default-border');
.should('have.class', 'default-border');
cy.get(selectors.defaultAvatar).should('exist'); cy.get(selectors.defaultAvatar).should('exist');
}); });
}); });

View File

@ -64,6 +64,8 @@ HOME_LOCATION='http://localhost:8000'
API_LOCATION='http://localhost:3000' API_LOCATION='http://localhost:3000'
FORUM_LOCATION='https://forum.freecodecamp.org' FORUM_LOCATION='https://forum.freecodecamp.org'
NEWS_LOCATION='https://www.freecodecamp.org/news' NEWS_LOCATION='https://www.freecodecamp.org/news'
RADIO_LOCATION='https://coderadio.freecodecamp.org'
CHINESE_HOME='https://chinese.freecodecamp.org'
# --------------------- # ---------------------
# Debugging Mode Keys # Debugging Mode Keys