feat(client): redesigned navigation (#40709)
Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
9014fff6c4
commit
58c6c54c67
@ -55,7 +55,10 @@
|
||||
"sign-out": "退出",
|
||||
"curriculum": "课程",
|
||||
"forum": "论坛",
|
||||
"radio": "Radio",
|
||||
"profile": "个人资料",
|
||||
"news": "News",
|
||||
"donate": "Donate",
|
||||
"update-settings": "更新我的账号设置",
|
||||
"sign-me-out": "退出登录 freeCodeCamp",
|
||||
"flag-user": "标记该用户的账号为滥用",
|
||||
@ -314,6 +317,7 @@
|
||||
"donate": {
|
||||
"title": "支持我们的非营利组织",
|
||||
"processing": "我们正在处理你的捐款。",
|
||||
"thanks": "Thanks for donating",
|
||||
"thank-you": "谢谢你成为我们的支持者。",
|
||||
"thank-you-2": "谢谢你成为 freeCodeCamp 的支持者。现在你已设置定期捐款。",
|
||||
"additional": "你可以使用这个链接 <0>{{url}}</0> 额外进行一次性捐款:",
|
||||
|
@ -57,7 +57,10 @@
|
||||
"sign-out": "Sign out",
|
||||
"curriculum": "Curriculum",
|
||||
"forum": "Forum",
|
||||
"radio": "Radio",
|
||||
"profile": "Profile",
|
||||
"news": "News",
|
||||
"donate": "Donate",
|
||||
"update-settings": "Update my account settings",
|
||||
"sign-me-out": "Sign me out of freeCodeCamp",
|
||||
"flag-user": "Flag This User's Account for Abuse",
|
||||
@ -316,6 +319,7 @@
|
||||
"donate": {
|
||||
"title": "Support our nonprofit",
|
||||
"processing": "We are processing your donation.",
|
||||
"thanks": "Thanks for donating",
|
||||
"thank-you": "Thank you for being a supporter.",
|
||||
"thank-you-2": "Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.",
|
||||
"additional": "You can make an additional one-time donation of any amount using this link: <0>{{url}}</0>",
|
||||
@ -376,7 +380,7 @@
|
||||
},
|
||||
"search": {
|
||||
"label": "Search",
|
||||
"placeholder": "Search 6,000+ tutorial",
|
||||
"placeholder": "Search 6,000+ tutorials",
|
||||
"see-results": "See all results for {{searchQuery}}",
|
||||
"no-tutorials": "No tutorials found",
|
||||
"try": "Looking for something? Try the search bar on this page.",
|
||||
|
@ -57,7 +57,10 @@
|
||||
"sign-out": "Cerrar sesión",
|
||||
"curriculum": "Plan de estudio",
|
||||
"forum": "Foro",
|
||||
"radio": "Radio",
|
||||
"profile": "Perfil",
|
||||
"news": "News",
|
||||
"donate": "Donate",
|
||||
"update-settings": "Actualizar la configuración de mi cuenta",
|
||||
"sign-me-out": "Cerrar sesión en freeCodeCamp",
|
||||
"flag-user": "Marcar la cuenta de este usuario por abuso",
|
||||
@ -316,6 +319,7 @@
|
||||
"donate": {
|
||||
"title": "Apoya a nuestra organización sin fines de lucro",
|
||||
"processing": "Estamos procesando tu donación.",
|
||||
"thanks": "Thanks for donating",
|
||||
"thank-you": "Gracias por tu apoyo.",
|
||||
"thank-you-2": "Gracias por apoyar a freeCodeCamp. Actualmente tienes una donación recurrente.",
|
||||
"additional": "Puede hacer una donación adicional por única vez de cualquier monto utilizando este enlace: <0>{{url}}</0>",
|
||||
|
@ -61,7 +61,10 @@ const translationsSchema = {
|
||||
'sign-out': 'Sign out',
|
||||
curriculum: 'Curriculum',
|
||||
forum: 'Forum',
|
||||
radio: 'Radio',
|
||||
profile: 'Profile',
|
||||
news: 'News',
|
||||
donate: 'Donate',
|
||||
'update-settings': 'Update my account settings',
|
||||
'sign-me-out': 'Sign me out of freeCodeCamp',
|
||||
'flag-user': "Flag This User's Account for Abuse",
|
||||
@ -365,6 +368,7 @@ const translationsSchema = {
|
||||
donate: {
|
||||
title: 'Support our nonprofit',
|
||||
processing: 'We are processing your donation.',
|
||||
thanks: 'Thanks for donating',
|
||||
'thank-you': 'Thank you for being a supporter.',
|
||||
'thank-you-2':
|
||||
'Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.',
|
||||
|
@ -1,13 +1,13 @@
|
||||
/* global expect */
|
||||
import React from 'react';
|
||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||
import renderer from 'react-test-renderer';
|
||||
/* import { useTranslation } from 'react-i18next';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import i18n from '../../../i18n/configForTests';*/
|
||||
import { UniversalNav } from './components/UniversalNav';
|
||||
import { AuthOrProfile } from './components/NavLinks';
|
||||
import { NavLinks } from './components/NavLinks';
|
||||
import AuthOrProfile from './components/AuthOrProfile';
|
||||
|
||||
describe('<UniversalNav />', () => {
|
||||
const UniversalNavProps = {
|
||||
@ -26,32 +26,48 @@ describe('<UniversalNav />', () => {
|
||||
});
|
||||
|
||||
describe('<NavLinks />', () => {
|
||||
it('shows Curriculum and Sign In buttons when not signed in', () => {
|
||||
it('has expected navigation links', () => {
|
||||
const landingPageProps = {
|
||||
fetchState: {
|
||||
pending: false
|
||||
},
|
||||
user: {
|
||||
isUserDonating: false,
|
||||
username: '',
|
||||
theme: 'default'
|
||||
},
|
||||
i18n: {
|
||||
language: 'en'
|
||||
},
|
||||
toggleNightMode: theme => theme
|
||||
};
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<AuthOrProfile {...landingPageProps} />);
|
||||
shallow.render(<NavLinks {...landingPageProps} />);
|
||||
const result = shallow.getRenderOutput();
|
||||
expect(
|
||||
hasRadioNavItem(result) &&
|
||||
hasForumNavItem(result) &&
|
||||
hasCurriculumNavItem(result) &&
|
||||
hasSignInButton(result)
|
||||
hasNewsNavItem(result) &&
|
||||
hasDonateNavItem(result)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AuthOrProfile />', () => {
|
||||
it('has avatar with default border for default users', () => {
|
||||
const defaultUserProps = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
picture: 'https://freecodecamp.org/image.png'
|
||||
},
|
||||
pending: false
|
||||
pending: false,
|
||||
pathName: '/learn'
|
||||
};
|
||||
|
||||
const componentTree = renderer
|
||||
.create(<AuthOrProfile {...defaultUserProps} />)
|
||||
.toJSON();
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<AuthOrProfile {...defaultUserProps} />);
|
||||
const componentTree = shallow.getRenderOutput();
|
||||
|
||||
expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy();
|
||||
});
|
||||
@ -63,11 +79,12 @@ describe('<NavLinks />', () => {
|
||||
picture: 'https://freecodecamp.org/image.png',
|
||||
isDonating: true
|
||||
},
|
||||
pending: false
|
||||
pending: false,
|
||||
pathName: '/learn'
|
||||
};
|
||||
const componentTree = renderer
|
||||
.create(<AuthOrProfile {...donatingUserProps} />)
|
||||
.toJSON();
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<AuthOrProfile {...donatingUserProps} />);
|
||||
const componentTree = shallow.getRenderOutput();
|
||||
|
||||
expect(avatarHasClass(componentTree, 'gold-border')).toBeTruthy();
|
||||
});
|
||||
@ -79,12 +96,13 @@ describe('<NavLinks />', () => {
|
||||
picture: 'https://freecodecamp.org/image.png',
|
||||
yearsTopContributor: [2020]
|
||||
},
|
||||
pending: false
|
||||
pending: false,
|
||||
pathName: '/learn'
|
||||
};
|
||||
|
||||
const componentTree = renderer
|
||||
.create(<AuthOrProfile {...topContributorUserProps} />)
|
||||
.toJSON();
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<AuthOrProfile {...topContributorUserProps} />);
|
||||
const componentTree = shallow.getRenderOutput();
|
||||
|
||||
expect(avatarHasClass(componentTree, 'blue-border')).toBeTruthy();
|
||||
});
|
||||
@ -96,41 +114,60 @@ describe('<NavLinks />', () => {
|
||||
isDonating: true,
|
||||
yearsTopContributor: [2020]
|
||||
},
|
||||
pending: false
|
||||
pending: false,
|
||||
pathName: '/learn'
|
||||
};
|
||||
const componentTree = renderer
|
||||
.create(<AuthOrProfile {...topDonatingContributorUserProps} />)
|
||||
.toJSON();
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<AuthOrProfile {...topDonatingContributorUserProps} />);
|
||||
const componentTree = shallow.getRenderOutput();
|
||||
expect(avatarHasClass(componentTree, 'purple-border')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
const navigationLinks = (component, navItem) => {
|
||||
return component.props.children[0].props.children[navItem].props.children
|
||||
.props;
|
||||
return component.props.children.props.children[navItem].props.children.props;
|
||||
};
|
||||
|
||||
const profileNavItem = component => component[2].children[0];
|
||||
const profileNavItem = component => component.props.children;
|
||||
|
||||
const hasDonateNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 0);
|
||||
return children === 'buttons.donate' && to === '/donate';
|
||||
};
|
||||
|
||||
const hasForumNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 0);
|
||||
const { children, to } = navigationLinks(component, 1);
|
||||
return (
|
||||
children === 'buttons.forum' && to === 'https://forum.freecodecamp.org'
|
||||
);
|
||||
};
|
||||
|
||||
const hasNewsNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 2);
|
||||
return (
|
||||
children === 'buttons.news' && to === 'https://www.freecodecamp.org/news'
|
||||
);
|
||||
};
|
||||
|
||||
const hasCurriculumNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 1);
|
||||
const { children, to } = navigationLinks(component, 3);
|
||||
return children === 'buttons.curriculum' && to === '/learn';
|
||||
};
|
||||
|
||||
const hasSignInButton = component =>
|
||||
component.props.children[1].props.children === 'buttons.sign-in';
|
||||
|
||||
const avatarHasClass = (componentTree, classes) => {
|
||||
// componentTree[1].children[0].children[1].props.className
|
||||
const hasRadioNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 5);
|
||||
return (
|
||||
profileNavItem(componentTree).children[1].props.className ===
|
||||
'avatar-container ' + classes
|
||||
children === 'buttons.radio' && to === 'https://coderadio.freecodecamp.org'
|
||||
);
|
||||
};
|
||||
|
||||
/* TODO: Apply this to Universalnav component
|
||||
const hasSignInButton = component =>
|
||||
component.props.children[1].props.children === 'buttons.sign-in';
|
||||
*/
|
||||
const avatarHasClass = (componentTree, classes) => {
|
||||
return (
|
||||
profileNavItem(componentTree).props.className ===
|
||||
'avatar-nav-link ' + classes
|
||||
);
|
||||
};
|
||||
|
@ -39,15 +39,6 @@ export function AuthOrProfile({ user, pathName, pending }) {
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<Link className='nav-link' to='/learn'>
|
||||
{t('buttons.curriculum')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className='nav-link' to={`/${user.username}`}>
|
||||
{t('buttons.profile')}
|
||||
</Link>
|
||||
<Link
|
||||
className={`avatar-nav-link ${badgeColorClass}`}
|
||||
to={`/${user.username}`}
|
||||
@ -58,7 +49,6 @@ export function AuthOrProfile({ user, pathName, pending }) {
|
||||
userName={user.username}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,21 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AuthOrProfile from './AuthOrProfile';
|
||||
|
||||
const MenuButton = props => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
aria-expanded={props.displayMenu}
|
||||
className={
|
||||
'toggle-button-nav' + (props.displayMenu ? ' reverse-toggle-color' : '')
|
||||
'toggle-button-nav' +
|
||||
(props.displayMenu ? ' reverse-toggle-color' : '')
|
||||
}
|
||||
onClick={props.onClick}
|
||||
ref={props.innerRef}
|
||||
>
|
||||
{t('buttons.menu')}
|
||||
</button>
|
||||
<span className='navatar'>
|
||||
<AuthOrProfile user={props.user} />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -24,7 +31,8 @@ MenuButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
displayMenu: PropTypes.bool.isRequired,
|
||||
innerRef: PropTypes.object,
|
||||
onClick: PropTypes.func.isRequired
|
||||
onClick: PropTypes.func.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
export default MenuButton;
|
||||
|
@ -1,89 +1,154 @@
|
||||
import React from 'react';
|
||||
import { Link, SkeletonSprite, AvatarRenderer } from '../../helpers';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import Login from '../components/Login';
|
||||
import { forumLocation } from '../../../../../config/env.json';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Link, SkeletonSprite } from '../../helpers';
|
||||
import { updateUserFlag } from '../../../redux/settings';
|
||||
import {
|
||||
forumLocation,
|
||||
radioLocation,
|
||||
newsLocation
|
||||
} from '../../../../../config/env.json';
|
||||
import createLanguageRedirect from '../../createLanguageRedirect';
|
||||
|
||||
const {
|
||||
availableLangs,
|
||||
i18nextCodes,
|
||||
langDisplayNames
|
||||
} = require('../../../../i18n/allLangs');
|
||||
|
||||
const locales = availableLangs.client;
|
||||
|
||||
const propTypes = {
|
||||
displayMenu: PropTypes.bool,
|
||||
fetchState: PropTypes.shape({ pending: PropTypes.bool }),
|
||||
i18n: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
toggleNightMode: PropTypes.func.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
export function AuthOrProfile({ user, pending }) {
|
||||
const { t } = useTranslation();
|
||||
const isUserDonating = user && user.isDonating;
|
||||
const isUserSignedIn = user && user.username;
|
||||
const isTopContributor =
|
||||
user && user.yearsTopContributor && user.yearsTopContributor.length > 0;
|
||||
const mapDispatchToProps = {
|
||||
toggleNightMode: theme => updateUserFlag({ theme })
|
||||
};
|
||||
|
||||
const CurriculumAndForumLinks = (
|
||||
<>
|
||||
<li>
|
||||
export class NavLinks extends Component {
|
||||
toggleTheme(currentTheme = 'default', toggleNightMode) {
|
||||
console.log('attempting to toggle night mode');
|
||||
toggleNightMode(currentTheme === 'night' ? 'default' : 'night');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
displayMenu,
|
||||
fetchState,
|
||||
i18n,
|
||||
t,
|
||||
toggleNightMode,
|
||||
user: { isUserDonating = false, username, theme }
|
||||
} = this.props;
|
||||
|
||||
const { pending } = fetchState;
|
||||
|
||||
return pending ? (
|
||||
<div className='nav-skeleton'>
|
||||
<SkeletonSprite />
|
||||
</div>
|
||||
) : (
|
||||
<div className='main-nav-group'>
|
||||
<ul className={'nav-list' + (displayMenu ? ' display-menu' : '')}>
|
||||
<li key='donate'>
|
||||
{isUserDonating ? (
|
||||
<span className='nav-link'>{t('donate.thanks')}</span>
|
||||
) : (
|
||||
<Link
|
||||
className='nav-link'
|
||||
external={true}
|
||||
sameTab={true}
|
||||
sameTab={false}
|
||||
to='/donate'
|
||||
>
|
||||
{t('buttons.donate')}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
<li key='forum'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
external={true}
|
||||
sameTab={false}
|
||||
to={forumLocation}
|
||||
>
|
||||
{t('buttons.forum')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<li key='news'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
external={true}
|
||||
sameTab={false}
|
||||
to={newsLocation}
|
||||
>
|
||||
{t('buttons.news')}
|
||||
</Link>
|
||||
</li>
|
||||
<li key='learn'>
|
||||
<Link className='nav-link' to='/learn'>
|
||||
{t('buttons.curriculum')}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
|
||||
if (pending) {
|
||||
return (
|
||||
<div className='nav-skeleton'>
|
||||
<SkeletonSprite />
|
||||
</div>
|
||||
);
|
||||
} else if (!isUserSignedIn) {
|
||||
return (
|
||||
<>
|
||||
{CurriculumAndForumLinks}
|
||||
<Login data-test-label='landing-small-cta'>
|
||||
{t('buttons.sign-in')}
|
||||
</Login>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{CurriculumAndForumLinks}
|
||||
<li>
|
||||
<Link className='nav-link' to={`/${user.username}`}>
|
||||
{username && (
|
||||
<li key='profile'>
|
||||
<Link className='nav-link' to={`/${username}`}>
|
||||
{t('buttons.profile')}
|
||||
<AvatarRenderer
|
||||
isDonating={isUserDonating}
|
||||
isTopContributor={isTopContributor}
|
||||
picture={user.picture}
|
||||
userName={user.username}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function NavLinks({ displayMenu, user, fetchState }) {
|
||||
const { pending } = fetchState;
|
||||
return (
|
||||
<div className='main-nav-group'>
|
||||
<ul className={'nav-list' + (displayMenu ? ' display-flex' : '')}>
|
||||
<AuthOrProfile pending={pending} user={user} />
|
||||
)}
|
||||
<li key='radio'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
external={true}
|
||||
sameTab={false}
|
||||
to={radioLocation}
|
||||
>
|
||||
{t('buttons.radio')}
|
||||
</Link>
|
||||
</li>
|
||||
<li key='theme'>
|
||||
<button
|
||||
className='nav-link'
|
||||
disabled={!username}
|
||||
onClick={() => this.toggleTheme(theme, toggleNightMode)}
|
||||
>
|
||||
{username
|
||||
? t('settings.labels.night-mode') +
|
||||
(theme === 'night' ? ' ✓' : '')
|
||||
: 'Sign in to change theme'}
|
||||
</button>
|
||||
</li>
|
||||
<li key='lang-header'>
|
||||
<span className='nav-link'>{t('footer.language')}</span>
|
||||
</li>
|
||||
{locales.map(lang => (
|
||||
<li key={'lang-' + lang}>
|
||||
<Link
|
||||
className='nav-link sub-link'
|
||||
to={createLanguageRedirect(lang)}
|
||||
>
|
||||
{langDisplayNames[lang]}
|
||||
{i18n.language === i18nextCodes[lang] ? ' ✓' : ''}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavLinks.propTypes = propTypes;
|
||||
NavLinks.displayName = 'NavLinks';
|
||||
export default NavLinks;
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(NavLinks));
|
||||
|
@ -17,11 +17,11 @@ export const UniversalNav = ({
|
||||
fetchState
|
||||
}) => (
|
||||
<nav
|
||||
className={'universal-nav nav-padding' + (displayMenu ? ' expand-nav' : '')}
|
||||
className={'universal-nav' + (displayMenu ? ' expand-nav' : '')}
|
||||
id='universal-nav'
|
||||
>
|
||||
<div
|
||||
className={'universal-nav-left' + (displayMenu ? ' display-flex' : '')}
|
||||
className={'universal-nav-left' + (displayMenu ? ' display-search' : '')}
|
||||
>
|
||||
<SearchBar innerRef={searchBarRef} />
|
||||
</div>
|
||||
@ -32,13 +32,14 @@ export const UniversalNav = ({
|
||||
</Link>
|
||||
</div>
|
||||
<div className='universal-nav-right main-nav'>
|
||||
<NavLinks displayMenu={displayMenu} fetchState={fetchState} user={user} />
|
||||
</div>
|
||||
<MenuButton
|
||||
displayMenu={displayMenu}
|
||||
innerRef={menuButtonRef}
|
||||
onClick={toggleDisplayMenu}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
<NavLinks displayMenu={displayMenu} fetchState={fetchState} user={user} />
|
||||
</nav>
|
||||
);
|
||||
|
||||
|
@ -3,10 +3,9 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
/* overflow-y: hidden; */
|
||||
height: 40px;
|
||||
font-size: 18px;
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-family: 'Roboto-Mono', sans-serif;
|
||||
height: var(--header-height);
|
||||
background: var(--theme-color);
|
||||
position: fixed;
|
||||
@ -25,22 +24,37 @@
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.universal-nav-left {
|
||||
display: flex;
|
||||
flex: 1 0 33%;
|
||||
margin-left: 0px;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.universal-nav-middle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 0 33%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-right: 10px;
|
||||
letter-spacing: 0.4px;
|
||||
white-space: nowrap;
|
||||
-ms-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.universal-nav-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex: 1 0 33%;
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
#universal-nav-logo {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin: 0px;
|
||||
margin: 0 auto;
|
||||
color: var(--gray-00);
|
||||
font-size: 1.7rem;
|
||||
line-height: 1em;
|
||||
@ -64,15 +78,19 @@
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: var(--theme-color);
|
||||
top: calc(var(--header-height) * 2);
|
||||
right: 0px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
width: 100vw;
|
||||
height: auto;
|
||||
margin: 0 0 0 -12px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
height: var(--header-height);
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.nav-list li {
|
||||
@ -81,32 +99,41 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-list li a.nav-link {
|
||||
.nav-link {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0 15px;
|
||||
align-items: center;
|
||||
padding: 2px 15px 0 25px;
|
||||
color: var(--gray-00);
|
||||
background-color: var(--gray-90);
|
||||
opacity: 1;
|
||||
white-space: nowrap;
|
||||
height: var(--header-height);
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-list li:last-child {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.nav-list span.nav-link {
|
||||
padding: 2px 15px 0 15px;
|
||||
background-color: var(--gray-75);
|
||||
}
|
||||
|
||||
.nav-list li:last-child a {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.nav-list li a.nav-link:hover {
|
||||
.nav-list a.nav-link:hover,
|
||||
.nav-list button.nav-link:hover {
|
||||
color: var(--theme-color);
|
||||
text-decoration: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.nav-list button:disabled {
|
||||
background-color: var(--gray-75);
|
||||
}
|
||||
|
||||
.nav-list button:disabled:hover {
|
||||
color: white;
|
||||
background-color: var(--gray-75);
|
||||
}
|
||||
|
||||
.nav-list li a.nav-link:focus {
|
||||
background: var(--theme-color);
|
||||
color: white;
|
||||
@ -136,15 +163,7 @@
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.universal-nav-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.toggle-button-nav {
|
||||
display: none;
|
||||
padding: 2px 14px 2px;
|
||||
border: 1px solid var(--gray-00);
|
||||
font-family: 'lato', sans-serif;
|
||||
@ -153,8 +172,8 @@
|
||||
outline: none;
|
||||
background-color: var(--theme-color);
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
height: auto;
|
||||
max-height: calc(var(--header-height) - 6px);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.toggle-button-nav:hover {
|
||||
@ -163,88 +182,61 @@
|
||||
border: 1px solid var(--gray-00);
|
||||
}
|
||||
|
||||
.nav-list li .avatar-container {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
opacity: 1;
|
||||
white-space: nowrap;
|
||||
background: transparent;
|
||||
height: calc(var(--header-height) - 8px);
|
||||
width: calc(var(--header-height) - 8px);
|
||||
.navatar {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nav-list li .avatar-containersvg {
|
||||
display: inline-block;
|
||||
.navatar .avatar-nav-link {
|
||||
height: 31px;
|
||||
width: 31px;
|
||||
}
|
||||
|
||||
.navatar .default-border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.navatar .avatar-container svg {
|
||||
display: inline;
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
.nav-list .avatar-container svg,
|
||||
.nav-list .avatar-container img {
|
||||
|
||||
.navatar .avatar-container svg,
|
||||
.navatar .avatar-container img {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-list .gold-border {
|
||||
.gold-border {
|
||||
border: 2px solid var(--yellow-gold);
|
||||
}
|
||||
|
||||
.nav-list .blue-border {
|
||||
.blue-border {
|
||||
border: 2px solid var(--blue-mid);
|
||||
}
|
||||
|
||||
.nav-list .purple-border {
|
||||
.purple-border {
|
||||
border: 2px solid var(--purple-mid);
|
||||
}
|
||||
|
||||
.nav-list .default-border {
|
||||
.default-border {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 300px) {
|
||||
.nav-list li a.nav-link {
|
||||
width: 50vw;
|
||||
text-align: center;
|
||||
}
|
||||
.expand-nav {
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
@media (max-width: 1079px) {
|
||||
.site-header {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.universal-nav-middle {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.universal-nav-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: var(--theme-color);
|
||||
top: calc(var(--header-height) * 2);
|
||||
right: 0px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
width: 100vw;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.display-flex {
|
||||
display: flex;
|
||||
.display-menu {
|
||||
display: inherit;
|
||||
text-align: left;
|
||||
margin-top: calc(-1 * var(--header-height));
|
||||
}
|
||||
|
||||
.toggle-button-nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.expand-nav {
|
||||
min-height: calc(3 * var(--header-height));
|
||||
}
|
||||
|
||||
.reverse-toggle-color {
|
||||
background-color: var(--gray-00);
|
||||
color: var(--theme-color);
|
||||
@ -255,15 +247,31 @@
|
||||
color: var(--theme-color);
|
||||
}
|
||||
|
||||
.universal-nav-left form {
|
||||
@media (max-width: 980px) {
|
||||
.universal-nav-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.universal-nav-middle {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.display-search {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.universal-nav-right {
|
||||
flex: 1 0 50%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.fcc_searchBar .ais-SearchBox-form {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: var(--header-height);
|
||||
left: 15px;
|
||||
max-width: calc(100vw - 350px);
|
||||
}
|
||||
|
||||
#universal-nav-logo {
|
||||
@ -272,40 +280,49 @@
|
||||
left: 17px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.expand-nav {
|
||||
min-height: calc(2 * var(--header-height));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1080px) {
|
||||
.universal-nav-middle {
|
||||
flex: 1 0 30%;
|
||||
margin-right: 0px;
|
||||
@media (max-width: 600px) {
|
||||
.nav-list {
|
||||
min-width: 100%;
|
||||
top: calc(var(--header-height) * 2);
|
||||
margin-top: 0;
|
||||
}
|
||||
.universal-nav-left {
|
||||
display: flex;
|
||||
flex: 1 0 35%;
|
||||
margin-left: 0px;
|
||||
|
||||
.fcc_searchBar .ais-SearchBox-form {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ais-SearchBox-input {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.ais-Hits {
|
||||
min-width: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.universal-nav {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.universal-nav {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
#universal-nav-logo {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.universal-nav-right {
|
||||
flex: 1 0 35%;
|
||||
margin-left: auto;
|
||||
}
|
||||
.main-nav-group {
|
||||
margin-left: auto;
|
||||
left: 5px;
|
||||
max-width: 45%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
.universal-nav-middle {
|
||||
flex: 1 0 33%;
|
||||
}
|
||||
.universal-nav-left {
|
||||
flex: 1 0 33%;
|
||||
}
|
||||
|
||||
.universal-nav-right {
|
||||
flex: 1 0 33%;
|
||||
}
|
||||
.signup-btn {
|
||||
max-height: calc(var(--header-height) - 6px);
|
||||
padding: 0 4px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
10
client/src/components/createLanguageRedirect.js
Normal file
10
client/src/components/createLanguageRedirect.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { homeLocation, chineseHome } from '../../../config/env.json';
|
||||
|
||||
const createLanguageRedirect = lang => {
|
||||
const path = window.location.pathname;
|
||||
if (lang === 'chinese') return `${chineseHome}${path}`;
|
||||
if (lang === 'english') return `${homeLocation}${path}`;
|
||||
return `${homeLocation}/${lang}${path}`;
|
||||
};
|
||||
|
||||
export default createLanguageRedirect;
|
@ -14,8 +14,8 @@
|
||||
--purple-dark: #5a01a7;
|
||||
--yellow-light: #ffc300;
|
||||
--yellow-dark: #4d3800;
|
||||
--blue-light: 153, 201, 255;
|
||||
--blue-dark: 0, 46, 173;
|
||||
--blue-light: rgb(153, 201, 255);
|
||||
--blue-dark: rgb(0, 46, 173);
|
||||
--green-light: #acd157;
|
||||
--blue-mid: #198eee;
|
||||
--purple-mid: darkviolet;
|
||||
|
@ -36,7 +36,7 @@
|
||||
|
||||
.fcc_searchBar .ais-Hits {
|
||||
top: 70px;
|
||||
width: calc(100vw - 30px);
|
||||
width: calc(100vw - 350px);
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ and arrow keys */
|
||||
}
|
||||
|
||||
.ais-SearchBox-input {
|
||||
width: calc(100vw - 30px);
|
||||
width: calc(100vw - 350px);
|
||||
}
|
||||
|
||||
.fcc_searchBar .ais-SearchBox-form {
|
||||
@ -136,7 +136,7 @@ and arrow keys */
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 1080px) {
|
||||
@media (min-width: 980px) {
|
||||
.ais-SearchBox-input {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
|
@ -7,7 +7,7 @@ const themeKey = 'fcc-theme';
|
||||
const defaultTheme = 'default';
|
||||
const nightTheme = 'night';
|
||||
|
||||
function setTheme(currentTheme = defaultTheme, theme) {
|
||||
export function setTheme(currentTheme = defaultTheme, theme) {
|
||||
if (currentTheme !== theme) {
|
||||
store.set(themeKey, theme);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ const {
|
||||
API_LOCATION: apiLocation,
|
||||
FORUM_LOCATION: forumLocation,
|
||||
NEWS_LOCATION: newsLocation,
|
||||
RADIO_LOCATION: radioLocation,
|
||||
CLIENT_LOCALE: clientLocale,
|
||||
CURRICULUM_LOCALE: curriculumLocale,
|
||||
SHOW_LOCALE_DROPDOWN_MENU: showLocaleDropdownMenu,
|
||||
@ -23,14 +24,17 @@ const {
|
||||
ALGOLIA_API_KEY: algoliaAPIKey,
|
||||
PAYPAL_CLIENT_ID: paypalClientId,
|
||||
DEPLOYMENT_ENV: deploymentEnv,
|
||||
SHOW_UPCOMING_CHANGES: showUpcomingChanges
|
||||
SHOW_UPCOMING_CHANGES: showUpcomingChanges,
|
||||
CHINESE_HOME: chineseHome
|
||||
} = process.env;
|
||||
|
||||
const locations = {
|
||||
homeLocation,
|
||||
apiLocation,
|
||||
forumLocation,
|
||||
newsLocation
|
||||
newsLocation,
|
||||
radioLocation,
|
||||
chineseHome
|
||||
};
|
||||
|
||||
module.exports = Object.assign(locations, {
|
||||
|
@ -5,18 +5,46 @@ const selectors = {
|
||||
smallCallToAction: "[data-test-label='landing-small-cta']",
|
||||
navigationLinks: '.nav-list',
|
||||
avatarContainer: '.avatar-container',
|
||||
defaultAvatar: '.avatar-container svg'
|
||||
defaultAvatar: '.avatar-container',
|
||||
menuButton: '.toggle-button-nav'
|
||||
};
|
||||
|
||||
let appHasStarted;
|
||||
function spyOnListener(win) {
|
||||
const addListener = win.EventTarget.prototype.addEventListener;
|
||||
win.EventTarget.prototype.addEventListener = function(name) {
|
||||
if (name === 'click') {
|
||||
appHasStarted = true;
|
||||
win.EventTarget.prototype.addEventListener = addListener;
|
||||
}
|
||||
return addListener.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
function waitForAppStart() {
|
||||
return new Promise(resolve => {
|
||||
const isReady = () => {
|
||||
if (appHasStarted) {
|
||||
return resolve();
|
||||
}
|
||||
return setTimeout(isReady, 0);
|
||||
};
|
||||
isReady();
|
||||
});
|
||||
}
|
||||
|
||||
describe('Navbar', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
cy.viewport(1200, 660);
|
||||
appHasStarted = false;
|
||||
cy.visit('/', {
|
||||
onBeforeLoad: spyOnListener
|
||||
}).then(waitForAppStart);
|
||||
cy.viewport(1300, 660);
|
||||
});
|
||||
|
||||
it('Should render properly', () => {
|
||||
cy.get('#universal-nav').should('be.visible');
|
||||
cy.get('#universal-nav').should('have.class', 'universal-nav nav-padding');
|
||||
cy.get('#universal-nav').should('have.class', 'universal-nav');
|
||||
});
|
||||
|
||||
it(
|
||||
@ -51,9 +79,10 @@ describe('Navbar', () => {
|
||||
|
||||
// have the curriculum and CTA on landing and /learn pages.
|
||||
it(
|
||||
'Should have `Forum` and `Curriculum` links on landing and learn pages' +
|
||||
'Should have `Radio`, `Forum`, and `Curriculum` links on landing and learn pages' +
|
||||
'page when not signed in',
|
||||
() => {
|
||||
cy.get(selectors.menuButton).click();
|
||||
cy.get(selectors.navigationLinks).contains('Forum');
|
||||
cy.get(selectors.navigationLinks)
|
||||
.contains('Curriculum')
|
||||
@ -61,14 +90,16 @@ describe('Navbar', () => {
|
||||
cy.url().should('include', '/learn');
|
||||
cy.get(selectors.navigationLinks).contains('Curriculum');
|
||||
cy.get(selectors.navigationLinks).contains('Forum');
|
||||
cy.get(selectors.navigationLinks).contains('Radio');
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'Should have `Sign in` link on landing and learn pages' +
|
||||
'page when not signed in',
|
||||
' when not signed in',
|
||||
() => {
|
||||
cy.contains(selectors.smallCallToAction, 'Sign in');
|
||||
cy.get(selectors.menuButton).click();
|
||||
cy.get(selectors.navigationLinks)
|
||||
.contains('Curriculum')
|
||||
.click();
|
||||
@ -77,17 +108,18 @@ describe('Navbar', () => {
|
||||
);
|
||||
|
||||
it('Should have `Profile` link when user is signed in', () => {
|
||||
cy.login()
|
||||
.get(selectors.navigationLinks)
|
||||
cy.login();
|
||||
cy.get('a[href*="/settings"]').should('be.visible');
|
||||
cy.get(selectors.menuButton).click();
|
||||
cy.get(selectors.navigationLinks)
|
||||
.contains('Profile')
|
||||
.click();
|
||||
cy.url().should('include', '/developmentuser');
|
||||
});
|
||||
|
||||
it('Should have a profile image with class `default-border`', () => {
|
||||
cy.login()
|
||||
.get(selectors.avatarContainer)
|
||||
.should('have.class', 'default-border');
|
||||
cy.login();
|
||||
cy.get(selectors.avatarContainer).should('have.class', 'default-border');
|
||||
cy.get(selectors.defaultAvatar).should('exist');
|
||||
});
|
||||
});
|
||||
|
@ -64,6 +64,8 @@ HOME_LOCATION='http://localhost:8000'
|
||||
API_LOCATION='http://localhost:3000'
|
||||
FORUM_LOCATION='https://forum.freecodecamp.org'
|
||||
NEWS_LOCATION='https://www.freecodecamp.org/news'
|
||||
RADIO_LOCATION='https://coderadio.freecodecamp.org'
|
||||
CHINESE_HOME='https://chinese.freecodecamp.org'
|
||||
|
||||
# ---------------------
|
||||
# Debugging Mode Keys
|
||||
|
Reference in New Issue
Block a user