fix(client): address nav UX issues (#40823)
Co-authored-by: nhcarrigan <nhcarrigan@gmail.com>
This commit is contained in:
@ -396,7 +396,8 @@
|
||||
"update-email-1": "更新你的邮件地址",
|
||||
"update-email-2": "在这里更新你的邮件地址:",
|
||||
"email": "邮箱",
|
||||
"and": "和"
|
||||
"and": "和",
|
||||
"change-theme": "Sign in to change theme."
|
||||
},
|
||||
"icons": {
|
||||
"gold-cup": "金奖杯",
|
||||
|
@ -398,7 +398,8 @@
|
||||
"update-email-1": "Update your email address",
|
||||
"update-email-2": "Update your email address here:",
|
||||
"email": "Email",
|
||||
"and": "and"
|
||||
"and": "and",
|
||||
"change-theme": "Sign in to change theme."
|
||||
},
|
||||
"icons": {
|
||||
"gold-cup": "Gold Cup",
|
||||
|
@ -398,7 +398,8 @@
|
||||
"update-email-1": "Actualiza tu correo electrónico",
|
||||
"update-email-2": "Actualiza tu correo electrónico aquí:",
|
||||
"email": "Correo electrónico",
|
||||
"and": "y"
|
||||
"and": "y",
|
||||
"change-theme": "Sign in to change theme."
|
||||
},
|
||||
"icons": {
|
||||
"gold-cup": "Copa de Oro",
|
||||
|
@ -470,7 +470,8 @@ const translationsSchema = {
|
||||
'update-email-1': 'Update your email address',
|
||||
'update-email-2': 'Update your email address here:',
|
||||
email: 'Email',
|
||||
and: 'and'
|
||||
and: 'and',
|
||||
'change-theme': 'Sign in to change theme.'
|
||||
},
|
||||
icons: {
|
||||
'gold-cup': 'Gold Cup',
|
||||
|
@ -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;
|
@ -13,32 +13,6 @@ exports[`<Footer /> matches snapshot 1`] = `
|
||||
<div
|
||||
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>
|
||||
footer.tax-exempt-status
|
||||
</p>
|
||||
|
@ -20,21 +20,6 @@
|
||||
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 {
|
||||
margin: 0 0 1.45rem;
|
||||
line-height: 30px;
|
||||
|
@ -2,11 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from '../helpers/Link';
|
||||
import LanguageMenu from './LanguageMenu';
|
||||
import './footer.css';
|
||||
|
||||
const { showLocaleDropdownMenu = false } = require('../../../config/env');
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.any
|
||||
};
|
||||
@ -26,7 +23,6 @@ function Footer() {
|
||||
<div className='footer-container'>
|
||||
<div className='footer-top'>
|
||||
<div className='footer-desc-col'>
|
||||
{showLocaleDropdownMenu ? <LanguageMenu /> : null}
|
||||
<p>{t('footer.tax-exempt-status')}</p>
|
||||
<p>{t('footer.mission-statement')}</p>
|
||||
<p>{t('footer.donation-initiatives')}</p>
|
||||
|
@ -1,21 +1,23 @@
|
||||
/* global expect */
|
||||
import React from 'react';
|
||||
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 { NavLinks } from './components/NavLinks';
|
||||
import AuthOrProfile from './components/AuthOrProfile';
|
||||
|
||||
import { apiLocation } from '../../../../config/env.json';
|
||||
|
||||
describe('<UniversalNav />', () => {
|
||||
const UniversalNavProps = {
|
||||
displayMenu: false,
|
||||
menuButtonRef: {},
|
||||
searchBarRef: {},
|
||||
toggleDisplayMenu: function() {},
|
||||
pathName: '/'
|
||||
pathName: '/',
|
||||
fetchState: {
|
||||
pending: false
|
||||
}
|
||||
};
|
||||
it('renders to the DOM', () => {
|
||||
const shallow = new ShallowRenderer();
|
||||
@ -26,14 +28,14 @@ describe('<UniversalNav />', () => {
|
||||
});
|
||||
|
||||
describe('<NavLinks />', () => {
|
||||
it('has expected navigation links', () => {
|
||||
it('has expected navigation links when not signed in', () => {
|
||||
const landingPageProps = {
|
||||
fetchState: {
|
||||
pending: false
|
||||
},
|
||||
user: {
|
||||
isUserDonating: false,
|
||||
username: '',
|
||||
isDonating: false,
|
||||
username: null,
|
||||
theme: 'default'
|
||||
},
|
||||
i18n: {
|
||||
@ -45,11 +47,70 @@ describe('<NavLinks />', () => {
|
||||
shallow.render(<NavLinks {...landingPageProps} />);
|
||||
const result = shallow.getRenderOutput();
|
||||
expect(
|
||||
hasRadioNavItem(result) &&
|
||||
hasForumNavItem(result) &&
|
||||
hasDonateNavItem(result) &&
|
||||
hasSignInNavItem(result) &&
|
||||
hasCurriculumNavItem(result) &&
|
||||
hasForumNavItem(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();
|
||||
});
|
||||
});
|
||||
@ -68,7 +129,6 @@ describe('<AuthOrProfile />', () => {
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<AuthOrProfile {...defaultUserProps} />);
|
||||
const componentTree = shallow.getRenderOutput();
|
||||
|
||||
expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -125,7 +185,7 @@ describe('<AuthOrProfile />', () => {
|
||||
});
|
||||
|
||||
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;
|
||||
@ -135,29 +195,66 @@ const hasDonateNavItem = component => {
|
||||
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 { children, to } = navigationLinks(component, 1);
|
||||
const { children, to } = navigationLinks(component, 5);
|
||||
return (
|
||||
children === 'buttons.forum' && to === 'https://forum.freecodecamp.org'
|
||||
children[0].props.children === 'buttons.forum' &&
|
||||
to === 'https://forum.freecodecamp.org/'
|
||||
);
|
||||
};
|
||||
|
||||
const hasNewsNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 2);
|
||||
const { children, to } = navigationLinks(component, 6);
|
||||
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 { children, to } = navigationLinks(component, 3);
|
||||
return children === 'buttons.curriculum' && to === '/learn';
|
||||
const hasRadioNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 7);
|
||||
return (
|
||||
children[0].props.children === 'buttons.radio' &&
|
||||
to === 'https://coderadio.freecodecamp.org'
|
||||
);
|
||||
};
|
||||
|
||||
const hasRadioNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 5);
|
||||
const hasSignOutNavItem = component => {
|
||||
const { children } = navigationLinks(component, 10);
|
||||
const signOutProps = children[1].props;
|
||||
|
||||
return (
|
||||
children === 'buttons.radio' && to === 'https://coderadio.freecodecamp.org'
|
||||
signOutProps.children === 'buttons.sign-out' &&
|
||||
signOutProps.href === `${apiLocation}/signout`
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,30 +2,71 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { Link, SkeletonSprite } from '../../helpers';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
// faCheck,
|
||||
faCheckSquare,
|
||||
faHeart,
|
||||
faSquare,
|
||||
faExternalLinkAlt
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from '../../helpers';
|
||||
import { updateUserFlag } from '../../../redux/settings';
|
||||
import {
|
||||
clientLocale,
|
||||
forumLocation,
|
||||
radioLocation,
|
||||
newsLocation
|
||||
apiLocation
|
||||
} from '../../../../../config/env.json';
|
||||
import createLanguageRedirect from '../../createLanguageRedirect';
|
||||
// import createLanguageRedirect from '../../createLanguageRedirect';
|
||||
import createExternalRedirect from '../../createExternalRedirects';
|
||||
|
||||
const {
|
||||
/* const {
|
||||
availableLangs,
|
||||
i18nextCodes,
|
||||
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 = {
|
||||
displayMenu: PropTypes.bool,
|
||||
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
|
||||
i18n: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
toggleDisplayMenu: PropTypes.func,
|
||||
toggleNightMode: PropTypes.func.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
@ -36,117 +77,134 @@ const mapDispatchToProps = {
|
||||
|
||||
export class NavLinks extends Component {
|
||||
toggleTheme(currentTheme = 'default', toggleNightMode) {
|
||||
console.log('attempting to toggle night mode');
|
||||
toggleNightMode(currentTheme === 'night' ? 'default' : 'night');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
displayMenu,
|
||||
// i18n,
|
||||
fetchState,
|
||||
i18n,
|
||||
t,
|
||||
// toggleDisplayMenu,
|
||||
toggleNightMode,
|
||||
user: { isUserDonating = false, username, theme }
|
||||
user: { isDonating = false, username, theme }
|
||||
} = this.props;
|
||||
|
||||
const { pending } = fetchState;
|
||||
|
||||
return pending ? (
|
||||
<div className='nav-skeleton'>
|
||||
<SkeletonSprite />
|
||||
</div>
|
||||
<div className='nav-skeleton' />
|
||||
) : (
|
||||
<div className='main-nav-group'>
|
||||
<ul className={'nav-list' + (displayMenu ? ' display-menu' : '')}>
|
||||
<li key='donate'>
|
||||
{isUserDonating ? (
|
||||
<span className='nav-link'>{t('donate.thanks')}</span>
|
||||
<div className={'nav-list' + (displayMenu ? ' display-menu' : '')}>
|
||||
{isDonating ? (
|
||||
<div className='nav-link nav-link-flex nav-link-header' key='donate'>
|
||||
<span>{t('donate.thanks')}</span>
|
||||
<FontAwesomeIcon icon={faHeart} />
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
className='nav-link'
|
||||
external={true}
|
||||
key='donate'
|
||||
sameTab={false}
|
||||
to='/donate'
|
||||
>
|
||||
{t('buttons.donate')}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
<li key='forum'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
external={true}
|
||||
sameTab={false}
|
||||
to={forumLocation}
|
||||
{!username && (
|
||||
<a
|
||||
className='nav-link nav-link-sign-in'
|
||||
href={`${apiLocation}/signin`}
|
||||
key='signin'
|
||||
>
|
||||
{t('buttons.forum')}
|
||||
</Link>
|
||||
</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.sign-in')}
|
||||
</a>
|
||||
)}
|
||||
<Link className='nav-link' key='learn' to='/learn'>
|
||||
{t('buttons.curriculum')}
|
||||
</Link>
|
||||
</li>
|
||||
{username && (
|
||||
<li key='profile'>
|
||||
<Link className='nav-link' to={`/${username}`}>
|
||||
{t('buttons.profile')}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li key='radio'>
|
||||
<>
|
||||
<Link
|
||||
className='nav-link'
|
||||
key='profile'
|
||||
sameTab={false}
|
||||
to={`/${username}`}
|
||||
>
|
||||
{t('buttons.profile')}
|
||||
</Link>
|
||||
<Link
|
||||
className='nav-link'
|
||||
key='settings'
|
||||
sameTab={false}
|
||||
to={`/settings`}
|
||||
>
|
||||
{t('buttons.settings')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<hr className='nav-line' />
|
||||
<Link
|
||||
className='nav-link nav-link-flex'
|
||||
external={true}
|
||||
key='forum'
|
||||
sameTab={false}
|
||||
to={createExternalRedirect('forum', { clientLocale })}
|
||||
>
|
||||
<span>{t('buttons.forum')}</span>
|
||||
<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}
|
||||
>
|
||||
{t('buttons.radio')}
|
||||
<span>{t('buttons.radio')}</span>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Link>
|
||||
</li>
|
||||
<li key='theme'>
|
||||
<hr className='nav-line' />
|
||||
<button
|
||||
className='nav-link'
|
||||
className={
|
||||
'nav-link nav-link-flex' + (!username ? ' nav-link-header' : '')
|
||||
}
|
||||
disabled={!username}
|
||||
key='theme'
|
||||
onClick={() => this.toggleTheme(theme, toggleNightMode)}
|
||||
>
|
||||
{username
|
||||
? t('settings.labels.night-mode') +
|
||||
(theme === 'night' ? ' ✓' : '')
|
||||
: 'Sign in to change theme'}
|
||||
{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>
|
||||
)}
|
||||
</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>
|
||||
{username && (
|
||||
<>
|
||||
<hr className='nav-line-2' />
|
||||
<a className='nav-link' href={`${apiLocation}/signout`}>
|
||||
{t('buttons.sign-out')}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Link } from '../../helpers';
|
||||
import { Link, SkeletonSprite } from '../../helpers';
|
||||
import NavLogo from './NavLogo';
|
||||
import SearchBar from '../../search/searchBar/SearchBar';
|
||||
import MenuButton from './MenuButton';
|
||||
@ -15,13 +15,17 @@ export const UniversalNav = ({
|
||||
searchBarRef,
|
||||
user,
|
||||
fetchState
|
||||
}) => (
|
||||
}) => {
|
||||
const { pending } = fetchState;
|
||||
return (
|
||||
<nav
|
||||
className={'universal-nav' + (displayMenu ? ' expand-nav' : '')}
|
||||
id='universal-nav'
|
||||
>
|
||||
<div
|
||||
className={'universal-nav-left' + (displayMenu ? ' display-search' : '')}
|
||||
className={
|
||||
'universal-nav-left' + (displayMenu ? ' display-search' : '')
|
||||
}
|
||||
>
|
||||
<SearchBar innerRef={searchBarRef} />
|
||||
</div>
|
||||
@ -32,16 +36,29 @@ export const UniversalNav = ({
|
||||
</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} fetchState={fetchState} user={user} />
|
||||
|
||||
<NavLinks
|
||||
displayMenu={displayMenu}
|
||||
fetchState={fetchState}
|
||||
toggleDisplayMenu={toggleDisplayMenu}
|
||||
user={user}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
UniversalNav.displayName = 'UniversalNav';
|
||||
export default UniversalNav;
|
||||
|
@ -66,6 +66,7 @@
|
||||
text-decoration: none;
|
||||
background-color: var(--theme-color);
|
||||
}
|
||||
|
||||
#universal-nav-logo:focus {
|
||||
background-color: inherit;
|
||||
}
|
||||
@ -90,19 +91,14 @@
|
||||
margin: 0 0 0 -12px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.nav-list li {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
margin: 0;
|
||||
padding: 2px 15px 0 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 15px 0 25px;
|
||||
color: var(--gray-00);
|
||||
background-color: var(--gray-90);
|
||||
opacity: 1;
|
||||
@ -113,42 +109,47 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-list span.nav-link {
|
||||
padding: 2px 15px 0 15px;
|
||||
background-color: var(--gray-75);
|
||||
}
|
||||
|
||||
.nav-list a.nav-link:hover,
|
||||
.nav-list button.nav-link:hover {
|
||||
.nav-link:hover,
|
||||
.nav-link:active {
|
||||
color: var(--theme-color);
|
||||
text-decoration: none;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-list button:disabled {
|
||||
background-color: var(--gray-75);
|
||||
.nav-link-header,
|
||||
.nav-link-header:hover,
|
||||
.nav-link-header:active {
|
||||
color: var(--gray-00);
|
||||
background-color: var(--gray-90);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.nav-list button:disabled:hover {
|
||||
color: white;
|
||||
background-color: var(--gray-75);
|
||||
.nav-link .fa-external-link-alt {
|
||||
color: var(--gray-45);
|
||||
}
|
||||
|
||||
.nav-list li a.nav-link:focus {
|
||||
background: var(--theme-color);
|
||||
color: white;
|
||||
.nav-link .fa-check,
|
||||
.nav-link .fa-check-square {
|
||||
width: 18px !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.nav-list li a.nav-link:focus:hover {
|
||||
color: var(--theme-color);
|
||||
background: white;
|
||||
.nav-link-lang {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.nav-list a.btn-cta {
|
||||
padding: 0 20px;
|
||||
height: 30px;
|
||||
margin-left: 19px;
|
||||
margin-right: 15px;
|
||||
.nav-link-flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav-link-sign-in {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-link-dull {
|
||||
color: var(--gray-45);
|
||||
}
|
||||
|
||||
.nav-skeleton {
|
||||
@ -247,6 +248,23 @@
|
||||
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) {
|
||||
.universal-nav-left {
|
||||
display: none;
|
||||
@ -306,7 +324,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
@media (max-width: 455px) {
|
||||
.universal-nav {
|
||||
padding: 0 5px;
|
||||
}
|
||||
@ -315,14 +333,26 @@
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.nav-link-sign-in {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navatar .signup-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#universal-nav-logo {
|
||||
left: 5px;
|
||||
max-width: 45%;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.signup-btn {
|
||||
max-height: calc(var(--header-height) - 6px);
|
||||
padding: 0 4px;
|
||||
margin-left: 2px;
|
||||
@media (max-width: 300px) {
|
||||
#universal-nav-logo {
|
||||
max-width: none;
|
||||
left: -170px;
|
||||
}
|
||||
}
|
||||
|
16
client/src/components/createExternalRedirects.js
Normal file
16
client/src/components/createExternalRedirects.js
Normal 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;
|
81
client/src/components/createExternalRedirects.test.js
Normal file
81
client/src/components/createExternalRedirects.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -54,7 +54,6 @@ FREECODECAMP_NODE_ENV='development'
|
||||
# Languages to build
|
||||
CLIENT_LOCALE=english
|
||||
CURRICULUM_LOCALE=english
|
||||
SHOW_LOCALE_DROPDOWN_MENU=true
|
||||
|
||||
# Show or hide WIP in progress challenges
|
||||
SHOW_UPCOMING_CHANGES=false
|
||||
|
Reference in New Issue
Block a user