diff --git a/client/i18n/locales/chinese/translations.json b/client/i18n/locales/chinese/translations.json
index 554d94a03c..bd9b801a4e 100644
--- a/client/i18n/locales/chinese/translations.json
+++ b/client/i18n/locales/chinese/translations.json
@@ -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> 额外进行一次性捐款:",
diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 96d48db7ba..9d178a8fe4 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -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.",
diff --git a/client/i18n/locales/espanol/translations.json b/client/i18n/locales/espanol/translations.json
index b86238bc08..e60d318a55 100644
--- a/client/i18n/locales/espanol/translations.json
+++ b/client/i18n/locales/espanol/translations.json
@@ -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>",
diff --git a/client/i18n/translations-schema.js b/client/i18n/translations-schema.js
index bbd5701b71..81c4600171 100644
--- a/client/i18n/translations-schema.js
+++ b/client/i18n/translations-schema.js
@@ -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.',
diff --git a/client/src/components/Header/Header.test.js b/client/src/components/Header/Header.test.js
index 6cf5ed56b3..3c2e4de31a 100644
--- a/client/src/components/Header/Header.test.js
+++ b/client/src/components/Header/Header.test.js
@@ -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('', () => {
const UniversalNavProps = {
@@ -26,32 +26,48 @@ describe('', () => {
});
describe('', () => {
- it('shows Curriculum and Sign In buttons when not signed in', () => {
+ it('has expected navigation links', () => {
const landingPageProps = {
- pending: false
+ fetchState: {
+ pending: false
+ },
+ user: {
+ isUserDonating: false,
+ username: '',
+ theme: 'default'
+ },
+ i18n: {
+ language: 'en'
+ },
+ toggleNightMode: theme => theme
};
const shallow = new ShallowRenderer();
- shallow.render();
+ shallow.render();
const result = shallow.getRenderOutput();
expect(
- hasForumNavItem(result) &&
+ hasRadioNavItem(result) &&
+ hasForumNavItem(result) &&
hasCurriculumNavItem(result) &&
- hasSignInButton(result)
+ hasNewsNavItem(result) &&
+ hasDonateNavItem(result)
).toBeTruthy();
});
+});
+describe('', () => {
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()
- .toJSON();
+ const shallow = new ShallowRenderer();
+ shallow.render();
+ const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'default-border')).toBeTruthy();
});
@@ -63,11 +79,12 @@ describe('', () => {
picture: 'https://freecodecamp.org/image.png',
isDonating: true
},
- pending: false
+ pending: false,
+ pathName: '/learn'
};
- const componentTree = renderer
- .create()
- .toJSON();
+ const shallow = new ShallowRenderer();
+ shallow.render();
+ const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'gold-border')).toBeTruthy();
});
@@ -79,12 +96,13 @@ describe('', () => {
picture: 'https://freecodecamp.org/image.png',
yearsTopContributor: [2020]
},
- pending: false
+ pending: false,
+ pathName: '/learn'
};
- const componentTree = renderer
- .create()
- .toJSON();
+ const shallow = new ShallowRenderer();
+ shallow.render();
+ const componentTree = shallow.getRenderOutput();
expect(avatarHasClass(componentTree, 'blue-border')).toBeTruthy();
});
@@ -96,41 +114,60 @@ describe('', () => {
isDonating: true,
yearsTopContributor: [2020]
},
- pending: false
+ pending: false,
+ pathName: '/learn'
};
- const componentTree = renderer
- .create()
- .toJSON();
+ const shallow = new ShallowRenderer();
+ shallow.render();
+ 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
);
};
diff --git a/client/src/components/Header/components/AuthOrProfile.js b/client/src/components/Header/components/AuthOrProfile.js
index 4b7dd9b3b4..c3bd20eee3 100644
--- a/client/src/components/Header/components/AuthOrProfile.js
+++ b/client/src/components/Header/components/AuthOrProfile.js
@@ -39,26 +39,16 @@ export function AuthOrProfile({ user, pathName, pending }) {
} else {
return (
<>
-
-
- {t('buttons.curriculum')}
-
-
-
-
- {t('buttons.profile')}
-
-
-
-
-
+
+
+
>
);
}
diff --git a/client/src/components/Header/components/MenuButton.js b/client/src/components/Header/components/MenuButton.js
index fefaf59011..2aba749603 100644
--- a/client/src/components/Header/components/MenuButton.js
+++ b/client/src/components/Header/components/MenuButton.js
@@ -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 (
-
+ <>
+
+
+
+
+ >
);
};
@@ -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;
diff --git a/client/src/components/Header/components/NavLinks.js b/client/src/components/Header/components/NavLinks.js
index 549c67e051..043a4ab885 100644
--- a/client/src/components/Header/components/NavLinks.js
+++ b/client/src/components/Header/components/NavLinks.js
@@ -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 = (
- <>
-
-
- {t('buttons.forum')}
-
-
-
-
- {t('buttons.curriculum')}
-
-
- >
- );
+export class NavLinks extends Component {
+ toggleTheme(currentTheme = 'default', toggleNightMode) {
+ console.log('attempting to toggle night mode');
+ toggleNightMode(currentTheme === 'night' ? 'default' : 'night');
+ }
- if (pending) {
- return (
+ render() {
+ const {
+ displayMenu,
+ fetchState,
+ i18n,
+ t,
+ toggleNightMode,
+ user: { isUserDonating = false, username, theme }
+ } = this.props;
+
+ const { pending } = fetchState;
+
+ return pending ? (
- );
- } else if (!isUserSignedIn) {
- return (
- <>
- {CurriculumAndForumLinks}
-
- {t('buttons.sign-in')}
-
- >
- );
- } else {
- return (
- <>
- {CurriculumAndForumLinks}
-
-
- {t('buttons.profile')}
-
-
-
- >
+ ) : (
+
+
+ -
+ {isUserDonating ? (
+ {t('donate.thanks')}
+ ) : (
+
+ {t('buttons.donate')}
+
+ )}
+
+ -
+
+ {t('buttons.forum')}
+
+
+ -
+
+ {t('buttons.news')}
+
+
+ -
+
+ {t('buttons.curriculum')}
+
+
+ {username && (
+ -
+
+ {t('buttons.profile')}
+
+
+ )}
+ -
+
+ {t('buttons.radio')}
+
+
+ -
+
+
+ -
+ {t('footer.language')}
+
+ {locales.map(lang => (
+ -
+
+ {langDisplayNames[lang]}
+ {i18n.language === i18nextCodes[lang] ? ' ✓' : ''}
+
+
+ ))}
+
+
);
}
}
-export function NavLinks({ displayMenu, user, fetchState }) {
- const { pending } = fetchState;
- return (
-
- );
-}
-
NavLinks.propTypes = propTypes;
NavLinks.displayName = 'NavLinks';
-export default NavLinks;
+
+export default connect(
+ null,
+ mapDispatchToProps
+)(withTranslation()(NavLinks));
diff --git a/client/src/components/Header/components/UniversalNav.js b/client/src/components/Header/components/UniversalNav.js
index a39c7f362e..9b0388d683 100644
--- a/client/src/components/Header/components/UniversalNav.js
+++ b/client/src/components/Header/components/UniversalNav.js
@@ -17,11 +17,11 @@ export const UniversalNav = ({
fetchState
}) => (
);
diff --git a/client/src/components/Header/components/universalNav.css b/client/src/components/Header/components/universalNav.css
index 362555d28d..7879081929 100644
--- a/client/src/components/Header/components/universalNav.css
+++ b/client/src/components/Header/components/universalNav.css
@@ -3,10 +3,9 @@
display: flex;
justify-content: space-between;
align-items: flex-start;
- /* overflow-y: hidden; */
height: 40px;
font-size: 18px;
- font-family: 'Lato', sans-serif;
+ font-family: 'Roboto-Mono', sans-serif;
height: var(--header-height);
background: var(--theme-color);
position: fixed;
@@ -25,22 +24,37 @@
font-family: 'Roboto Mono', monospace;
}
+.universal-nav-left {
+ display: flex;
+ flex: 1 0 33%;
+ margin-left: 0px;
+ z-index: 2000;
+}
+
.universal-nav-middle {
display: flex;
align-items: center;
+ flex: 1 0 33%;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
- margin-right: 10px;
letter-spacing: 0.4px;
white-space: nowrap;
-ms-overflow-scrolling: touch;
}
+.universal-nav-right {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ flex: 1 0 33%;
+ height: var(--header-height);
+}
+
#universal-nav-logo {
flex-shrink: 0;
display: block;
- margin: 0px;
+ margin: 0 auto;
color: var(--gray-00);
font-size: 1.7rem;
line-height: 1em;
@@ -64,15 +78,19 @@
}
.nav-list {
- display: flex;
+ display: none;
+ position: absolute;
+ background-color: var(--theme-color);
+ top: calc(var(--header-height) * 2);
+ right: 0px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ width: 100vw;
+ height: auto;
margin: 0 0 0 -12px;
padding: 0;
list-style: none;
- align-items: center;
-}
-
-.nav-list {
- height: var(--header-height);
+ max-width: 300px;
}
.nav-list li {
@@ -81,32 +99,41 @@
padding: 0;
}
-.nav-list li a.nav-link {
+.nav-link {
display: flex;
- margin: 0;
- padding: 0 15px;
+ align-items: center;
+ padding: 2px 15px 0 25px;
color: var(--gray-00);
+ background-color: var(--gray-90);
opacity: 1;
white-space: nowrap;
height: var(--header-height);
+ width: 100%;
align-items: center;
+ border: none;
}
-.nav-list li:last-child {
- display: flex;
- flex-direction: row;
+.nav-list span.nav-link {
+ padding: 2px 15px 0 15px;
+ background-color: var(--gray-75);
}
-.nav-list li:last-child a {
- padding-right: 15px;
-}
-
-.nav-list li a.nav-link:hover {
+.nav-list a.nav-link:hover,
+.nav-list button.nav-link:hover {
color: var(--theme-color);
text-decoration: none;
background: white;
}
+.nav-list button:disabled {
+ background-color: var(--gray-75);
+}
+
+.nav-list button:disabled:hover {
+ color: white;
+ background-color: var(--gray-75);
+}
+
.nav-list li a.nav-link:focus {
background: var(--theme-color);
color: white;
@@ -136,15 +163,7 @@
margin-right: 25px;
}
-.universal-nav-right {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- height: var(--header-height);
-}
-
.toggle-button-nav {
- display: none;
padding: 2px 14px 2px;
border: 1px solid var(--gray-00);
font-family: 'lato', sans-serif;
@@ -153,8 +172,8 @@
outline: none;
background-color: var(--theme-color);
cursor: pointer;
- margin-top: 4px;
- height: auto;
+ max-height: calc(var(--header-height) - 6px);
+ margin-right: 10px;
}
.toggle-button-nav:hover {
@@ -163,100 +182,88 @@
border: 1px solid var(--gray-00);
}
-.nav-list li .avatar-container {
- display: block;
- padding: 0;
- margin-left: 15px;
- opacity: 1;
- white-space: nowrap;
- background: transparent;
- height: calc(var(--header-height) - 8px);
- width: calc(var(--header-height) - 8px);
+.navatar {
+ display: contents;
}
-.nav-list li .avatar-containersvg {
- display: inline-block;
+.navatar .avatar-nav-link {
+ height: 31px;
+ width: 31px;
+}
+
+.navatar .default-border {
+ border: none;
+}
+
+.navatar .avatar-container svg {
+ display: inline;
background: var(--secondary-background);
}
-.nav-list .avatar-container svg,
-.nav-list .avatar-container img {
+
+.navatar .avatar-container svg,
+.navatar .avatar-container img {
object-fit: cover;
width: 100%;
height: 100%;
}
-.nav-list .gold-border {
+.gold-border {
border: 2px solid var(--yellow-gold);
}
-.nav-list .blue-border {
+.blue-border {
border: 2px solid var(--blue-mid);
}
-.nav-list .purple-border {
+.purple-border {
border: 2px solid var(--purple-mid);
}
-.nav-list .default-border {
+.default-border {
border: 2px solid transparent;
}
-@media (max-width: 300px) {
- .nav-list li a.nav-link {
- width: 50vw;
- text-align: center;
- }
+.expand-nav {
+ height: var(--header-height);
}
-@media (max-width: 1079px) {
- .site-header {
- padding-right: 0;
- padding-left: 0;
- }
- .universal-nav-middle {
- margin-right: 0;
- }
+.display-menu {
+ display: inherit;
+ text-align: left;
+ margin-top: calc(-1 * var(--header-height));
+}
+.toggle-button-nav {
+ display: flex;
+}
+
+.reverse-toggle-color {
+ background-color: var(--gray-00);
+ color: var(--theme-color);
+}
+
+.reverse-toggle-color:hover {
+ background-color: var(--gray-00);
+ color: var(--theme-color);
+}
+
+@media (max-width: 980px) {
.universal-nav-left {
display: none;
}
- .nav-list {
- display: none;
- position: absolute;
- background-color: var(--theme-color);
- top: calc(var(--header-height) * 2);
- right: 0px;
- flex-wrap: wrap;
+ .universal-nav-middle {
+ flex: none;
+ }
+
+ .display-search {
+ display: initial;
+ }
+
+ .universal-nav-right {
+ flex: 1 0 50%;
+ display: flex;
justify-content: flex-end;
- width: 100vw;
- height: auto;
- }
-
- .display-flex {
- display: flex;
- }
-
- .toggle-button-nav {
- display: flex;
- }
-
- .expand-nav {
- min-height: calc(3 * var(--header-height));
- }
-
- .reverse-toggle-color {
- background-color: var(--gray-00);
- color: var(--theme-color);
- }
-
- .reverse-toggle-color:hover {
- background-color: var(--gray-00);
- color: var(--theme-color);
- }
-
- .universal-nav-left form {
- display: none;
}
.fcc_searchBar .ais-SearchBox-form {
@@ -264,6 +271,7 @@
position: absolute;
top: var(--header-height);
left: 15px;
+ max-width: calc(100vw - 350px);
}
#universal-nav-logo {
@@ -272,40 +280,49 @@
left: 17px;
top: 0px;
}
+
+ .expand-nav {
+ min-height: calc(2 * var(--header-height));
+ }
}
-@media (min-width: 1080px) {
- .universal-nav-middle {
- flex: 1 0 30%;
- margin-right: 0px;
+@media (max-width: 600px) {
+ .nav-list {
+ min-width: 100%;
+ top: calc(var(--header-height) * 2);
+ margin-top: 0;
}
- .universal-nav-left {
- display: flex;
- flex: 1 0 35%;
- margin-left: 0px;
+
+ .fcc_searchBar .ais-SearchBox-form {
+ max-width: 100%;
}
+
+ .ais-SearchBox-input {
+ min-width: 100%;
+ }
+
+ .ais-Hits {
+ min-width: calc(100% - 30px);
+ }
+}
+
+@media (max-width: 400px) {
+ .universal-nav {
+ padding: 0 5px;
+ }
+
+ .universal-nav {
+ padding: 0 5px;
+ }
+
#universal-nav-logo {
- margin-left: auto;
- margin-right: auto;
- }
- .universal-nav-right {
- flex: 1 0 35%;
- margin-left: auto;
- }
- .main-nav-group {
- margin-left: auto;
+ left: 5px;
+ max-width: 45%;
}
}
-@media (min-width: 1500px) {
- .universal-nav-middle {
- flex: 1 0 33%;
- }
- .universal-nav-left {
- flex: 1 0 33%;
- }
-
- .universal-nav-right {
- flex: 1 0 33%;
- }
+.signup-btn {
+ max-height: calc(var(--header-height) - 6px);
+ padding: 0 4px;
+ margin-left: 2px;
}
diff --git a/client/src/components/createLanguageRedirect.js b/client/src/components/createLanguageRedirect.js
new file mode 100644
index 0000000000..edef855bea
--- /dev/null
+++ b/client/src/components/createLanguageRedirect.js
@@ -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;
diff --git a/client/src/components/layouts/variables.css b/client/src/components/layouts/variables.css
index 4fd5ac5585..176c804d40 100644
--- a/client/src/components/layouts/variables.css
+++ b/client/src/components/layouts/variables.css
@@ -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;
diff --git a/client/src/components/search/searchBar/searchbar.css b/client/src/components/search/searchBar/searchbar.css
index fbb28a3133..24dd7653ad 100644
--- a/client/src/components/search/searchBar/searchbar.css
+++ b/client/src/components/search/searchBar/searchbar.css
@@ -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;
diff --git a/client/src/redux/night-mode-saga.js b/client/src/redux/night-mode-saga.js
index 7723f06d99..b6109450bd 100644
--- a/client/src/redux/night-mode-saga.js
+++ b/client/src/redux/night-mode-saga.js
@@ -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);
}
diff --git a/config/env.js b/config/env.js
index 2d011ed33e..0f70cbe608 100644
--- a/config/env.js
+++ b/config/env.js
@@ -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, {
diff --git a/cypress/integration/learn/common-components/navbar.js b/cypress/integration/learn/common-components/navbar.js
index 7dc440717a..ada403c44d 100644
--- a/cypress/integration/learn/common-components/navbar.js
+++ b/cypress/integration/learn/common-components/navbar.js
@@ -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');
});
});
diff --git a/sample.env b/sample.env
index 7138618cf3..bacf27e9b5 100644
--- a/sample.env
+++ b/sample.env
@@ -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