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}} 额外进行一次性捐款:", 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}}", @@ -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}}", 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