fix(client): address nav UX issues (#40823)

Co-authored-by: nhcarrigan <nhcarrigan@gmail.com>
This commit is contained in:
Tom
2021-01-30 00:43:06 -06:00
committed by Mrugesh Mohapatra
parent 1875984423
commit c56a9c966f
15 changed files with 485 additions and 278 deletions

View File

@ -396,7 +396,8 @@
"update-email-1": "更新你的邮件地址", "update-email-1": "更新你的邮件地址",
"update-email-2": "在这里更新你的邮件地址:", "update-email-2": "在这里更新你的邮件地址:",
"email": "邮箱", "email": "邮箱",
"and": "和" "and": "和",
"change-theme": "Sign in to change theme."
}, },
"icons": { "icons": {
"gold-cup": "金奖杯", "gold-cup": "金奖杯",

View File

@ -398,7 +398,8 @@
"update-email-1": "Update your email address", "update-email-1": "Update your email address",
"update-email-2": "Update your email address here:", "update-email-2": "Update your email address here:",
"email": "Email", "email": "Email",
"and": "and" "and": "and",
"change-theme": "Sign in to change theme."
}, },
"icons": { "icons": {
"gold-cup": "Gold Cup", "gold-cup": "Gold Cup",

View File

@ -398,7 +398,8 @@
"update-email-1": "Actualiza tu correo electrónico", "update-email-1": "Actualiza tu correo electrónico",
"update-email-2": "Actualiza tu correo electrónico aquí:", "update-email-2": "Actualiza tu correo electrónico aquí:",
"email": "Correo electrónico", "email": "Correo electrónico",
"and": "y" "and": "y",
"change-theme": "Sign in to change theme."
}, },
"icons": { "icons": {
"gold-cup": "Copa de Oro", "gold-cup": "Copa de Oro",

View File

@ -470,7 +470,8 @@ const translationsSchema = {
'update-email-1': 'Update your email address', 'update-email-1': 'Update your email address',
'update-email-2': 'Update your email address here:', 'update-email-2': 'Update your email address here:',
email: 'Email', email: 'Email',
and: 'and' and: 'and',
'change-theme': 'Sign in to change theme.'
}, },
icons: { icons: {
'gold-cup': 'Gold Cup', 'gold-cup': 'Gold Cup',

View File

@ -1,50 +0,0 @@
/* eslint-disable jsx-a11y/no-onchange */
import React from 'react';
import { useTranslation } from 'react-i18next';
const {
availableLangs,
i18nextCodes,
langDisplayNames
} = require('../../../i18n/allLangs');
const { homeLocation } = require('../../../config/env');
const locales = availableLangs.client;
const LanguageMenu = () => {
const { i18n, t } = useTranslation();
const i18nLanguage = i18n.language;
const currentLanguage = Object.keys(i18nextCodes).find(
key => i18nextCodes[key] === i18nLanguage
);
const changeLanguage = e => {
const path = window.location.pathname;
if (e.target.value === 'espanol') {
window.location.replace(`${homeLocation}/espanol${path}`);
} else {
window.location.replace(`${homeLocation}${path}`);
}
};
return (
<div className='language-menu'>
<label>
{t('footer.language')}
<select onChange={e => changeLanguage(e)} value={currentLanguage}>
{locales.map((lang, i) => {
return (
<option key={i} value={lang}>
{langDisplayNames[lang]}
</option>
);
})}
</select>
</label>
</div>
);
};
export default LanguageMenu;

View File

@ -13,32 +13,6 @@ exports[`<Footer /> matches snapshot 1`] = `
<div <div
className="footer-desc-col" className="footer-desc-col"
> >
<div
className="language-menu"
>
<label>
footer.language
<select
onChange={[Function]}
>
<option
value="english"
>
English
</option>
<option
value="espanol"
>
Español
</option>
<option
value="chinese"
>
中文
</option>
</select>
</label>
</div>
<p> <p>
footer.tax-exempt-status footer.tax-exempt-status
</p> </p>

View File

@ -20,21 +20,6 @@
overflow-x: hidden; overflow-x: hidden;
} }
.footer-container .language-menu {
display: flex;
margin-bottom: 10px;
}
.footer-container .language-menu label {
font-weight: normal;
}
.footer-container .language-menu select {
padding: 0 5px;
margin-left: 10px;
font-weight: normal;
}
.footer-container p { .footer-container p {
margin: 0 0 1.45rem; margin: 0 0 1.45rem;
line-height: 30px; line-height: 30px;

View File

@ -2,11 +2,8 @@ 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 Link from '../helpers/Link'; import Link from '../helpers/Link';
import LanguageMenu from './LanguageMenu';
import './footer.css'; import './footer.css';
const { showLocaleDropdownMenu = false } = require('../../../config/env');
const propTypes = { const propTypes = {
children: PropTypes.any children: PropTypes.any
}; };
@ -26,7 +23,6 @@ function Footer() {
<div className='footer-container'> <div className='footer-container'>
<div className='footer-top'> <div className='footer-top'>
<div className='footer-desc-col'> <div className='footer-desc-col'>
{showLocaleDropdownMenu ? <LanguageMenu /> : null}
<p>{t('footer.tax-exempt-status')}</p> <p>{t('footer.tax-exempt-status')}</p>
<p>{t('footer.mission-statement')}</p> <p>{t('footer.mission-statement')}</p>
<p>{t('footer.donation-initiatives')}</p> <p>{t('footer.donation-initiatives')}</p>

View File

@ -1,21 +1,23 @@
/* 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 { useTranslation } from 'react-i18next';
import { I18nextProvider } from 'react-i18next';
import i18n from '../../../i18n/configForTests';*/
import { UniversalNav } from './components/UniversalNav'; import { UniversalNav } from './components/UniversalNav';
import { NavLinks } from './components/NavLinks'; import { NavLinks } from './components/NavLinks';
import AuthOrProfile from './components/AuthOrProfile'; import AuthOrProfile from './components/AuthOrProfile';
import { apiLocation } from '../../../../config/env.json';
describe('<UniversalNav />', () => { describe('<UniversalNav />', () => {
const UniversalNavProps = { const UniversalNavProps = {
displayMenu: false, displayMenu: false,
menuButtonRef: {}, menuButtonRef: {},
searchBarRef: {}, searchBarRef: {},
toggleDisplayMenu: function() {}, toggleDisplayMenu: function() {},
pathName: '/' pathName: '/',
fetchState: {
pending: false
}
}; };
it('renders to the DOM', () => { it('renders to the DOM', () => {
const shallow = new ShallowRenderer(); const shallow = new ShallowRenderer();
@ -26,14 +28,14 @@ describe('<UniversalNav />', () => {
}); });
describe('<NavLinks />', () => { describe('<NavLinks />', () => {
it('has expected navigation links', () => { it('has expected navigation links when not signed in', () => {
const landingPageProps = { const landingPageProps = {
fetchState: { fetchState: {
pending: false pending: false
}, },
user: { user: {
isUserDonating: false, isDonating: false,
username: '', username: null,
theme: 'default' theme: 'default'
}, },
i18n: { i18n: {
@ -45,11 +47,70 @@ describe('<NavLinks />', () => {
shallow.render(<NavLinks {...landingPageProps} />); shallow.render(<NavLinks {...landingPageProps} />);
const result = shallow.getRenderOutput(); const result = shallow.getRenderOutput();
expect( expect(
hasRadioNavItem(result) && hasDonateNavItem(result) &&
hasForumNavItem(result) && hasSignInNavItem(result) &&
hasCurriculumNavItem(result) && hasCurriculumNavItem(result) &&
hasForumNavItem(result) &&
hasNewsNavItem(result) && hasNewsNavItem(result) &&
hasDonateNavItem(result) hasRadioNavItem(result)
).toBeTruthy();
});
it('has expected navigation links when signed in', () => {
const landingPageProps = {
fetchState: {
pending: false
},
user: {
isDonating: false,
username: 'nhcarrigan',
theme: 'default'
},
i18n: {
language: 'en'
},
toggleNightMode: theme => theme
};
const shallow = new ShallowRenderer();
shallow.render(<NavLinks {...landingPageProps} />);
const result = shallow.getRenderOutput();
expect(
hasDonateNavItem(result) &&
hasCurriculumNavItem(result) &&
hasProfileAndSettingsNavItems(result, landingPageProps.user.username) &&
hasForumNavItem(result) &&
hasNewsNavItem(result) &&
hasRadioNavItem(result) &&
hasSignOutNavItem(result)
).toBeTruthy();
});
it('has expected navigation links when signed in and donating', () => {
const landingPageProps = {
fetchState: {
pending: false
},
user: {
isDonating: true,
username: 'moT01',
theme: 'default'
},
i18n: {
language: 'en'
},
toggleNightMode: theme => theme
};
const shallow = new ShallowRenderer();
shallow.render(<NavLinks {...landingPageProps} />);
const result = shallow.getRenderOutput();
expect(
hasThanksForDonating(result) &&
hasCurriculumNavItem(result) &&
hasProfileAndSettingsNavItems(result, landingPageProps.user.username) &&
hasForumNavItem(result) &&
hasNewsNavItem(result) &&
hasRadioNavItem(result) &&
hasSignOutNavItem(result)
).toBeTruthy(); ).toBeTruthy();
}); });
}); });
@ -68,7 +129,6 @@ describe('<AuthOrProfile />', () => {
const shallow = new ShallowRenderer(); const shallow = new ShallowRenderer();
shallow.render(<AuthOrProfile {...defaultUserProps} />); shallow.render(<AuthOrProfile {...defaultUserProps} />);
const componentTree = shallow.getRenderOutput(); const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy(); expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy();
}); });
@ -125,7 +185,7 @@ describe('<AuthOrProfile />', () => {
}); });
const navigationLinks = (component, navItem) => { const navigationLinks = (component, navItem) => {
return component.props.children.props.children[navItem].props.children.props; return component.props.children[navItem].props;
}; };
const profileNavItem = component => component.props.children; const profileNavItem = component => component.props.children;
@ -135,29 +195,66 @@ const hasDonateNavItem = component => {
return children === 'buttons.donate' && to === '/donate'; return children === 'buttons.donate' && to === '/donate';
}; };
const hasThanksForDonating = component => {
const { children } = navigationLinks(component, 0);
return children[0].props.children === 'donate.thanks';
};
const hasSignInNavItem = component => {
const { children } = navigationLinks(component, 1);
return children === 'buttons.sign-in';
};
const hasCurriculumNavItem = component => {
const { children, to } = navigationLinks(component, 2);
return children === 'buttons.curriculum' && to === '/learn';
};
const hasProfileAndSettingsNavItems = (component, username) => {
const fragment = navigationLinks(component, 3);
const profile = fragment.children[0].props;
const settings = fragment.children[1].props;
const hasProfile =
profile.children === 'buttons.profile' && profile.to === `/${username}`;
const hasSettings =
settings.children === 'buttons.settings' && settings.to === '/settings';
return hasProfile && hasSettings;
};
const hasForumNavItem = component => { const hasForumNavItem = component => {
const { children, to } = navigationLinks(component, 1); const { children, to } = navigationLinks(component, 5);
return ( return (
children === 'buttons.forum' && to === 'https://forum.freecodecamp.org' children[0].props.children === 'buttons.forum' &&
to === 'https://forum.freecodecamp.org/'
); );
}; };
const hasNewsNavItem = component => { const hasNewsNavItem = component => {
const { children, to } = navigationLinks(component, 2); const { children, to } = navigationLinks(component, 6);
return ( return (
children === 'buttons.news' && to === 'https://www.freecodecamp.org/news' children[0].props.children === 'buttons.news' &&
to === 'https://www.freecodecamp.org/news'
); );
}; };
const hasCurriculumNavItem = component => { const hasRadioNavItem = component => {
const { children, to } = navigationLinks(component, 3); const { children, to } = navigationLinks(component, 7);
return children === 'buttons.curriculum' && to === '/learn'; return (
children[0].props.children === 'buttons.radio' &&
to === 'https://coderadio.freecodecamp.org'
);
}; };
const hasRadioNavItem = component => { const hasSignOutNavItem = component => {
const { children, to } = navigationLinks(component, 5); const { children } = navigationLinks(component, 10);
const signOutProps = children[1].props;
return ( return (
children === 'buttons.radio' && to === 'https://coderadio.freecodecamp.org' signOutProps.children === 'buttons.sign-out' &&
signOutProps.href === `${apiLocation}/signout`
); );
}; };

View File

@ -2,30 +2,71 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Link, SkeletonSprite } from '../../helpers'; import {
// faCheck,
faCheckSquare,
faHeart,
faSquare,
faExternalLinkAlt
} from '@fortawesome/free-solid-svg-icons';
import { Link } from '../../helpers';
import { updateUserFlag } from '../../../redux/settings'; import { updateUserFlag } from '../../../redux/settings';
import { import {
clientLocale, clientLocale,
forumLocation,
radioLocation, radioLocation,
newsLocation apiLocation
} from '../../../../../config/env.json'; } from '../../../../../config/env.json';
import createLanguageRedirect from '../../createLanguageRedirect'; // import createLanguageRedirect from '../../createLanguageRedirect';
import createExternalRedirect from '../../createExternalRedirects';
const { /* const {
availableLangs, availableLangs,
i18nextCodes, i18nextCodes,
langDisplayNames langDisplayNames
} = require('../../../../i18n/allLangs'); } = require('../../../../i18n/allLangs'); */
const locales = availableLangs.client; // const locales = availableLangs.client;
// The linter was complaining about inline comments. Add the code below above
// the sign out button when the language menu is ready to be added
/*
<div className='nav-link nav-link-header' key='lang-header'>
{t('footer.language')}
</div>
{locales.map(lang =>
// current lang is a button that closes the menu
i18n.language === i18nextCodes[lang] ? (
<button
className='nav-link nav-link-lang nav-link-flex'
onClick={() => toggleDisplayMenu()}
>
<span>{langDisplayNames[lang]}</span>
<FontAwesomeIcon icon={faCheck} />
</button>
) : (
<Link
className='nav-link nav-link-lang nav-link-flex'
external={true}
// Todo: should treat other lang client application links as external??
key={'lang-' + lang}
to={createLanguageRedirect({
clientLocale,
lang
})}
>
{langDisplayNames[lang]}
</Link>
)
)
*/
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, i18n: PropTypes.object,
t: PropTypes.func, t: PropTypes.func,
toggleDisplayMenu: PropTypes.func,
toggleNightMode: PropTypes.func.isRequired, toggleNightMode: PropTypes.func.isRequired,
user: PropTypes.object user: PropTypes.object
}; };
@ -36,117 +77,134 @@ const mapDispatchToProps = {
export class NavLinks extends Component { export class NavLinks extends Component {
toggleTheme(currentTheme = 'default', toggleNightMode) { toggleTheme(currentTheme = 'default', toggleNightMode) {
console.log('attempting to toggle night mode');
toggleNightMode(currentTheme === 'night' ? 'default' : 'night'); toggleNightMode(currentTheme === 'night' ? 'default' : 'night');
} }
render() { render() {
const { const {
displayMenu, displayMenu,
// i18n,
fetchState, fetchState,
i18n,
t, t,
// toggleDisplayMenu,
toggleNightMode, toggleNightMode,
user: { isUserDonating = false, username, theme } user: { isDonating = false, username, theme }
} = this.props; } = this.props;
const { pending } = fetchState; const { pending } = fetchState;
return pending ? ( return pending ? (
<div className='nav-skeleton'> <div className='nav-skeleton' />
<SkeletonSprite />
</div>
) : ( ) : (
<div className='main-nav-group'> <div className={'nav-list' + (displayMenu ? ' display-menu' : '')}>
<ul className={'nav-list' + (displayMenu ? ' display-menu' : '')}> {isDonating ? (
<li key='donate'> <div className='nav-link nav-link-flex nav-link-header' key='donate'>
{isUserDonating ? ( <span>{t('donate.thanks')}</span>
<span className='nav-link'>{t('donate.thanks')}</span> <FontAwesomeIcon icon={faHeart} />
) : ( </div>
<Link ) : (
className='nav-link' <Link
external={true} className='nav-link'
sameTab={false} external={true}
to='/donate' key='donate'
> sameTab={false}
{t('buttons.donate')} to='/donate'
</Link> >
)} {t('buttons.donate')}
</li> </Link>
<li key='forum'> )}
{!username && (
<a
className='nav-link nav-link-sign-in'
href={`${apiLocation}/signin`}
key='signin'
>
{t('buttons.sign-in')}
</a>
)}
<Link className='nav-link' key='learn' to='/learn'>
{t('buttons.curriculum')}
</Link>
{username && (
<>
<Link <Link
className='nav-link' className='nav-link'
external={true} key='profile'
sameTab={false} sameTab={false}
to={forumLocation} to={`/${username}`}
> >
{t('buttons.forum')} {t('buttons.profile')}
</Link> </Link>
</li>
<li key='news'>
<Link <Link
className='nav-link' className='nav-link'
external={true} key='settings'
sameTab={false} sameTab={false}
to={newsLocation} to={`/settings`}
> >
{t('buttons.news')} {t('buttons.settings')}
</Link> </Link>
</li> </>
<li key='learn'> )}
<Link className='nav-link' to='/learn'> <hr className='nav-line' />
{t('buttons.curriculum')} <Link
</Link> className='nav-link nav-link-flex'
</li> external={true}
{username && ( key='forum'
<li key='profile'> sameTab={false}
<Link className='nav-link' to={`/${username}`}> to={createExternalRedirect('forum', { clientLocale })}
{t('buttons.profile')} >
</Link> <span>{t('buttons.forum')}</span>
</li> <FontAwesomeIcon icon={faExternalLinkAlt} />
</Link>
<Link
className='nav-link nav-link-flex'
external={true}
key='news'
sameTab={false}
to={createExternalRedirect('news', { clientLocale })}
>
<span>{t('buttons.news')}</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</Link>
<Link
className='nav-link nav-link-flex'
external={true}
key='radio'
sameTab={false}
to={radioLocation}
>
<span>{t('buttons.radio')}</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</Link>
<hr className='nav-line' />
<button
className={
'nav-link nav-link-flex' + (!username ? ' nav-link-header' : '')
}
disabled={!username}
key='theme'
onClick={() => this.toggleTheme(theme, toggleNightMode)}
>
{username ? (
<>
<span>{t('settings.labels.night-mode')}</span>
{theme === 'night' ? (
<FontAwesomeIcon icon={faCheckSquare} />
) : (
<FontAwesomeIcon icon={faSquare} />
)}
</>
) : (
<span className='nav-link-dull'>{t('misc.change-theme')}</span>
)} )}
<li key='radio'> </button>
<Link {username && (
className='nav-link' <>
external={true} <hr className='nav-line-2' />
sameTab={false} <a className='nav-link' href={`${apiLocation}/signout`}>
to={radioLocation} {t('buttons.sign-out')}
> </a>
{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'
// Todo: should treat other lang client application links as external??
external={true}
to={createLanguageRedirect({
clientLocale,
lang
})}
>
{langDisplayNames[lang]}
{i18n.language === i18nextCodes[lang] ? ' ✓' : ''}
</Link>
</li>
))}
</ul>
</div> </div>
); );
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from '../../helpers'; import { Link, SkeletonSprite } from '../../helpers';
import NavLogo from './NavLogo'; import NavLogo from './NavLogo';
import SearchBar from '../../search/searchBar/SearchBar'; import SearchBar from '../../search/searchBar/SearchBar';
import MenuButton from './MenuButton'; import MenuButton from './MenuButton';
@ -15,33 +15,50 @@ export const UniversalNav = ({
searchBarRef, searchBarRef,
user, user,
fetchState fetchState
}) => ( }) => {
<nav const { pending } = fetchState;
className={'universal-nav' + (displayMenu ? ' expand-nav' : '')} return (
id='universal-nav' <nav
> className={'universal-nav' + (displayMenu ? ' expand-nav' : '')}
<div id='universal-nav'
className={'universal-nav-left' + (displayMenu ? ' display-search' : '')}
> >
<SearchBar innerRef={searchBarRef} /> <div
</div> className={
<div className='universal-nav-middle'> 'universal-nav-left' + (displayMenu ? ' display-search' : '')
<Link id='universal-nav-logo' to='/learn'> }
<NavLogo /> >
<span className='sr-only'>freeCodeCamp.org</span> <SearchBar innerRef={searchBarRef} />
</Link> </div>
</div> <div className='universal-nav-middle'>
<div className='universal-nav-right main-nav'> <Link id='universal-nav-logo' to='/learn'>
<MenuButton <NavLogo />
<span className='sr-only'>freeCodeCamp.org</span>
</Link>
</div>
<div className='universal-nav-right main-nav'>
{pending ? (
<div className='nav-skeleton'>
<SkeletonSprite />
</div>
) : (
<MenuButton
displayMenu={displayMenu}
innerRef={menuButtonRef}
onClick={toggleDisplayMenu}
user={user}
/>
)}
</div>
<NavLinks
displayMenu={displayMenu} displayMenu={displayMenu}
innerRef={menuButtonRef} fetchState={fetchState}
onClick={toggleDisplayMenu} toggleDisplayMenu={toggleDisplayMenu}
user={user} user={user}
/> />
</div> </nav>
<NavLinks displayMenu={displayMenu} fetchState={fetchState} user={user} /> );
</nav> };
);
UniversalNav.displayName = 'UniversalNav'; UniversalNav.displayName = 'UniversalNav';
export default UniversalNav; export default UniversalNav;

View File

@ -66,6 +66,7 @@
text-decoration: none; text-decoration: none;
background-color: var(--theme-color); background-color: var(--theme-color);
} }
#universal-nav-logo:focus { #universal-nav-logo:focus {
background-color: inherit; background-color: inherit;
} }
@ -90,19 +91,14 @@
margin: 0 0 0 -12px; margin: 0 0 0 -12px;
padding: 0; padding: 0;
list-style: none; list-style: none;
max-width: 300px; max-width: 250px;
}
.nav-list li {
display: block;
margin: 0;
padding: 0;
} }
.nav-link { .nav-link {
margin: 0;
padding: 2px 15px 0 15px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 15px 0 25px;
color: var(--gray-00); color: var(--gray-00);
background-color: var(--gray-90); background-color: var(--gray-90);
opacity: 1; opacity: 1;
@ -113,42 +109,47 @@
border: none; border: none;
} }
.nav-list span.nav-link { .nav-link:hover,
padding: 2px 15px 0 15px; .nav-link:active {
background-color: var(--gray-75);
}
.nav-list a.nav-link:hover,
.nav-list button.nav-link:hover {
color: var(--theme-color); color: var(--theme-color);
text-decoration: none; text-decoration: none;
background: white; background: white;
cursor: pointer;
} }
.nav-list button:disabled { .nav-link-header,
background-color: var(--gray-75); .nav-link-header:hover,
.nav-link-header:active {
color: var(--gray-00);
background-color: var(--gray-90);
cursor: default;
} }
.nav-list button:disabled:hover { .nav-link .fa-external-link-alt {
color: white; color: var(--gray-45);
background-color: var(--gray-75);
} }
.nav-list li a.nav-link:focus { .nav-link .fa-check,
background: var(--theme-color); .nav-link .fa-check-square {
color: white; width: 18px !important;
height: auto !important;
} }
.nav-list li a.nav-link:focus:hover { .nav-link-lang {
color: var(--theme-color); padding-left: 30px;
background: white;
} }
.nav-list a.btn-cta { .nav-link-flex {
padding: 0 20px; display: flex;
height: 30px; justify-content: space-between;
margin-left: 19px; }
margin-right: 15px;
.nav-link-sign-in {
display: none;
}
.nav-link-dull {
color: var(--gray-45);
} }
.nav-skeleton { .nav-skeleton {
@ -247,6 +248,23 @@
color: var(--theme-color); color: var(--theme-color);
} }
.nav-line,
.nav-line-2 {
border-color: var(--gray-45);
width: 100%;
margin: 0;
}
.nav-line-2 {
border-top-width: 2px;
}
.signup-btn {
max-height: calc(var(--header-height) - 6px);
padding: 0 8px;
margin-left: 2px;
}
@media (max-width: 980px) { @media (max-width: 980px) {
.universal-nav-left { .universal-nav-left {
display: none; display: none;
@ -306,7 +324,7 @@
} }
} }
@media (max-width: 400px) { @media (max-width: 455px) {
.universal-nav { .universal-nav {
padding: 0 5px; padding: 0 5px;
} }
@ -315,14 +333,26 @@
padding: 0 5px; padding: 0 5px;
} }
.nav-link-sign-in {
display: flex;
}
.navatar .signup-btn {
display: none;
}
.navatar {
display: none;
}
#universal-nav-logo { #universal-nav-logo {
left: 5px; max-width: 60%;
max-width: 45%;
} }
} }
.signup-btn { @media (max-width: 300px) {
max-height: calc(var(--header-height) - 6px); #universal-nav-logo {
padding: 0 4px; max-width: none;
margin-left: 2px; left: -170px;
}
} }

View File

@ -0,0 +1,16 @@
import { forumLocation } from '../../config/env.json';
const createExternalRedirect = (page, { clientLocale }) => {
const isNotEnglish = clientLocale !== 'english';
if (clientLocale === 'chinese') {
return `https://chinese.freecodecamp.org/${page}`;
}
if (page === 'forum') {
return `${forumLocation}/${isNotEnglish ? 'c/' + clientLocale + '/' : ''}`;
}
return `https://www.freecodecamp.org/${
isNotEnglish ? clientLocale + '/news' : 'news'
}`;
};
export default createExternalRedirect;

View File

@ -0,0 +1,81 @@
/* global expect */
import createExternalRedirect from './createExternalRedirects';
describe('createExternalRedirects', () => {
describe('english redirects', () => {
const envVars = {
clientLocale: 'english'
};
const englishForumUrl = 'https://forum.freecodecamp.org/';
const englishNewsUrl = 'https://www.freecodecamp.org/news';
it('should generate correct forum link', () => {
const receivedUrl = createExternalRedirect('forum', { ...envVars });
expect(receivedUrl).toBe(englishForumUrl);
});
it('should generate correct news link', () => {
const receivedUrl = createExternalRedirect('news', { ...envVars });
expect(receivedUrl).toBe(englishNewsUrl);
});
});
describe('chinese redirects', () => {
const envVars = {
clientLocale: 'chinese'
};
const englishForumUrl = 'https://chinese.freecodecamp.org/forum';
const englishNewsUrl = 'https://chinese.freecodecamp.org/news';
it('should generate correct forum link', () => {
const receivedUrl = createExternalRedirect('forum', { ...envVars });
expect(receivedUrl).toBe(englishForumUrl);
});
it('should generate correct news link', () => {
const receivedUrl = createExternalRedirect('news', { ...envVars });
expect(receivedUrl).toBe(englishNewsUrl);
});
});
describe('spanish redirects', () => {
const envVars = {
clientLocale: 'espanol'
};
const englishForumUrl = 'https://forum.freecodecamp.org/c/espanol/';
const englishNewsUrl = 'https://www.freecodecamp.org/espanol/news';
it('should generate correct forum link', () => {
const receivedUrl = createExternalRedirect('forum', { ...envVars });
expect(receivedUrl).toBe(englishForumUrl);
});
it('should generate correct news link', () => {
const receivedUrl = createExternalRedirect('news', { ...envVars });
expect(receivedUrl).toBe(englishNewsUrl);
});
});
describe('french redirects', () => {
const envVars = {
clientLocale: 'francais'
};
const englishForumUrl = 'https://forum.freecodecamp.org/c/francais/';
const englishNewsUrl = 'https://www.freecodecamp.org/francais/news';
it('should generate correct forum link', () => {
const receivedUrl = createExternalRedirect('forum', { ...envVars });
expect(receivedUrl).toBe(englishForumUrl);
});
it('should generate correct news link', () => {
const receivedUrl = createExternalRedirect('news', { ...envVars });
expect(receivedUrl).toBe(englishNewsUrl);
});
});
});

View File

@ -54,7 +54,6 @@ FREECODECAMP_NODE_ENV='development'
# Languages to build # Languages to build
CLIENT_LOCALE=english CLIENT_LOCALE=english
CURRICULUM_LOCALE=english CURRICULUM_LOCALE=english
SHOW_LOCALE_DROPDOWN_MENU=true
# Show or hide WIP in progress challenges # Show or hide WIP in progress challenges
SHOW_UPCOMING_CHANGES=false SHOW_UPCOMING_CHANGES=false