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-1": "更新你的邮件地址",
|
||||||
"update-email-2": "在这里更新你的邮件地址:",
|
"update-email-2": "在这里更新你的邮件地址:",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"and": "和"
|
"and": "和",
|
||||||
|
"change-theme": "Sign in to change theme."
|
||||||
},
|
},
|
||||||
"icons": {
|
"icons": {
|
||||||
"gold-cup": "金奖杯",
|
"gold-cup": "金奖杯",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
<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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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
|
# 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
|
||||||
|
Reference in New Issue
Block a user