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

View File

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

View File

@ -57,7 +57,10 @@
"sign-out": "Cerrar sesión",
"curriculum": "Plan de estudio",
"forum": "Foro",
"radio": "Radio",
"profile": "Perfil",
"news": "News",
"donate": "Donate",
"update-settings": "Actualizar la configuración de mi cuenta",
"sign-me-out": "Cerrar sesión en freeCodeCamp",
"flag-user": "Marcar la cuenta de este usuario por abuso",
@ -316,6 +319,7 @@
"donate": {
"title": "Apoya a nuestra organización sin fines de lucro",
"processing": "Estamos procesando tu donación.",
"thanks": "Thanks for donating",
"thank-you": "Gracias por tu apoyo.",
"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>",

View File

@ -61,7 +61,10 @@ const translationsSchema = {
'sign-out': 'Sign out',
curriculum: 'Curriculum',
forum: 'Forum',
radio: 'Radio',
profile: 'Profile',
news: 'News',
donate: 'Donate',
'update-settings': 'Update my account settings',
'sign-me-out': 'Sign me out of freeCodeCamp',
'flag-user': "Flag This User's Account for Abuse",
@ -365,6 +368,7 @@ const translationsSchema = {
donate: {
title: 'Support our nonprofit',
processing: 'We are processing your donation.',
thanks: 'Thanks for donating',
'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.',

View File

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

View File

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

View File

@ -1,89 +1,154 @@
import React from 'react';
import { Link, SkeletonSprite, AvatarRenderer } from '../../helpers';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Login from '../components/Login';
import { forumLocation } from '../../../../../config/env.json';
import { useTranslation } from 'react-i18next';
import { withTranslation } from 'react-i18next';
import { Link, SkeletonSprite } from '../../helpers';
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 = {
displayMenu: PropTypes.bool,
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
i18n: PropTypes.object,
t: PropTypes.func,
toggleNightMode: PropTypes.func.isRequired,
user: PropTypes.object
};
export function AuthOrProfile({ user, pending }) {
const { t } = useTranslation();
const isUserDonating = user && user.isDonating;
const isUserSignedIn = user && user.username;
const isTopContributor =
user && user.yearsTopContributor && user.yearsTopContributor.length > 0;
const mapDispatchToProps = {
toggleNightMode: theme => updateUserFlag({ theme })
};
const CurriculumAndForumLinks = (
<>
<li>
export class NavLinks extends Component {
toggleTheme(currentTheme = 'default', toggleNightMode) {
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
className='nav-link'
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}
>
{t('buttons.forum')}
</Link>
</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'>
{t('buttons.curriculum')}
</Link>
</li>
</>
);
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}`}>
{username && (
<li key='profile'>
<Link className='nav-link' to={`/${username}`}>
{t('buttons.profile')}
<AvatarRenderer
isDonating={isUserDonating}
isTopContributor={isTopContributor}
picture={user.picture}
userName={user.username}
/>
</Link>
</li>
</>
)}
<li key='radio'>
<Link
className='nav-link'
external={true}
sameTab={false}
to={radioLocation}
>
{t('buttons.radio')}
</Link>
</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>
</div>
);
}
}
export function NavLinks({ displayMenu, user, fetchState }) {
const { pending } = fetchState;
return (
<div className='main-nav-group'>
<ul className={'nav-list' + (displayMenu ? ' display-flex' : '')}>
<AuthOrProfile pending={pending} user={user} />
</ul>
</div>
);
}
NavLinks.propTypes = propTypes;
NavLinks.displayName = 'NavLinks';
export default NavLinks;
export default connect(
null,
mapDispatchToProps
)(withTranslation()(NavLinks));

View File

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

View File

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

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;
--yellow-light: #ffc300;
--yellow-dark: #4d3800;
--blue-light: 153, 201, 255;
--blue-dark: 0, 46, 173;
--blue-light: rgb(153, 201, 255);
--blue-dark: rgb(0, 46, 173);
--green-light: #acd157;
--blue-mid: #198eee;
--purple-mid: darkviolet;

View File

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

View File

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

View File

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

View File

@ -5,18 +5,46 @@ const selectors = {
smallCallToAction: "[data-test-label='landing-small-cta']",
navigationLinks: '.nav-list',
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', () => {
beforeEach(() => {
cy.visit('/');
cy.viewport(1200, 660);
appHasStarted = false;
cy.visit('/', {
onBeforeLoad: spyOnListener
}).then(waitForAppStart);
cy.viewport(1300, 660);
});
it('Should render properly', () => {
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(
@ -51,9 +79,10 @@ describe('Navbar', () => {
// have the curriculum and CTA on landing and /learn pages.
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',
() => {
cy.get(selectors.menuButton).click();
cy.get(selectors.navigationLinks).contains('Forum');
cy.get(selectors.navigationLinks)
.contains('Curriculum')
@ -61,14 +90,16 @@ describe('Navbar', () => {
cy.url().should('include', '/learn');
cy.get(selectors.navigationLinks).contains('Curriculum');
cy.get(selectors.navigationLinks).contains('Forum');
cy.get(selectors.navigationLinks).contains('Radio');
}
);
it(
'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.get(selectors.menuButton).click();
cy.get(selectors.navigationLinks)
.contains('Curriculum')
.click();
@ -77,17 +108,18 @@ describe('Navbar', () => {
);
it('Should have `Profile` link when user is signed in', () => {
cy.login()
.get(selectors.navigationLinks)
cy.login();
cy.get('a[href*="/settings"]').should('be.visible');
cy.get(selectors.menuButton).click();
cy.get(selectors.navigationLinks)
.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.login();
cy.get(selectors.avatarContainer).should('have.class', 'default-border');
cy.get(selectors.defaultAvatar).should('exist');
});
});

View File

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