fix(client): remove algolia and hot keys modules from landing pages (#42394)

* fix(client): remove algolia and hot keys from landing pages

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2021-06-24 12:50:36 +03:00
committed by GitHub
parent db79a0a29a
commit b3f2c64de8
19 changed files with 332 additions and 215 deletions

View File

@ -26,7 +26,7 @@ import {
renderSignInEmail
} from '../utils';
import { blocklistedUsernames } from '../../server/utils/constants.js';
import { blocklistedUsernames } from '../../../../config/constants';
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
import { saveUser, observeMethod } from '../../server/utils/rx.js';
import { getEmailSender } from '../../server/utils/url-utils';

View File

@ -395,7 +395,8 @@
"analytics": "A bar chart and line graph",
"shield": "A shield with a checkmark",
"tensorflow": "Tensorflow icon",
"algorithm": "Branching nodes"
"algorithm": "Branching nodes",
"magnifier": "magnifier"
},
"aria": {
"fcc-logo": "freeCodeCamp Logo",

View File

@ -0,0 +1,24 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
const Magnifier = (): JSX.Element => {
const { t } = useTranslation();
return (
<>
<span className='sr-only'>{t('icons.Magnifier')}</span>
<svg
className='ais-SearchBox-submitIcon'
height='10'
viewBox='0 0 40 40'
width='10'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z' />
</svg>
</>
);
};
Magnifier.displayName = 'Magnifier';
export default Magnifier;

View File

@ -3,10 +3,17 @@ import PropTypes from 'prop-types';
import { Link, SkeletonSprite } from '../../helpers';
import NavLogo from './NavLogo';
import SearchBar from '../../search/searchBar/SearchBar';
import MenuButton from './MenuButton';
import NavLinks from './NavLinks';
import './universalNav.css';
import { isLanding } from '../../../utils/path-parsers';
import Loadable from '@loadable/component';
const SearchBar = Loadable(() => import('../../search/searchBar/SearchBar'));
const SearchBarOptimized = Loadable(() =>
import('../../search/searchBar/search-bar-optimized')
);
export const UniversalNav = ({
displayMenu,
@ -17,6 +24,14 @@ export const UniversalNav = ({
fetchState
}) => {
const { pending } = fetchState;
const search =
typeof window !== `undefined` && isLanding(window.location.pathname) ? (
<SearchBarOptimized />
) : (
<SearchBar innerRef={searchBarRef} />
);
return (
<nav
className={'universal-nav' + (displayMenu ? ' expand-nav' : '')}
@ -27,7 +42,7 @@ export const UniversalNav = ({
'universal-nav-left' + (displayMenu ? ' display-search' : '')
}
>
<SearchBar innerRef={searchBarRef} />
{search}
</div>
<div className='universal-nav-middle'>
<Link id='universal-nav-logo' to='/learn'>

View File

@ -21,7 +21,6 @@ import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
import { isBrowser } from '../../../utils';
import WithInstantSearch from '../search/WithInstantSearch';
import OfflineWarning from '../OfflineWarning';
import Flash from '../Flash';
import Header from '../Header';
@ -201,17 +200,15 @@ class DefaultLayout extends Component {
/>
<style>{fontawesome.dom.css()}</style>
</Helmet>
<WithInstantSearch>
<div className={`default-layout`}>
<Header fetchState={fetchState} user={user} />
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
{hasMessage && flashMessage ? (
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
) : null}
{children}
</div>
{showFooter && <Footer />}
</WithInstantSearch>
<div className={`default-layout`}>
<Header fetchState={fetchState} user={user} />
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
{hasMessage && flashMessage ? (
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
) : null}
{children}
</div>
{showFooter && <Footer />}
</div>
);
}

View File

@ -9,6 +9,8 @@ import { isEqual } from 'lodash-es';
import { withTranslation } from 'react-i18next';
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
import WithInstantSearch from '../WithInstantSearch';
import {
isSearchDropdownEnabledSelector,
isSearchBarFocusedSelector,
@ -177,33 +179,39 @@ export class SearchBar extends Component {
const placeholder = t('search.placeholder');
return (
<div className='fcc_searchBar' data-testid='fcc_searchBar' ref={innerRef}>
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
<div className='fcc_search_wrapper'>
<label className='fcc_sr_only' htmlFor='fcc_instantsearch'>
{t('search.label')}
</label>
<ObserveKeys except={['Space']}>
<SearchBox
focusShortcuts={[83, 191]}
onChange={this.handleChange}
onFocus={this.handleFocus}
onSubmit={this.handleSearch}
showLoadingIndicator={false}
translations={{ placeholder }}
/>
</ObserveKeys>
{isDropdownEnabled && isSearchFocused && (
<SearchHits
handleHits={this.handleHits}
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
selectedIndex={index}
/>
)}
</div>
</HotKeys>
</div>
<WithInstantSearch>
<div
className='fcc_searchBar'
data-testid='fcc_searchBar'
ref={innerRef}
>
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
<div className='fcc_search_wrapper'>
<label className='fcc_sr_only' htmlFor='fcc_instantsearch'>
{t('search.label')}
</label>
<ObserveKeys except={['Space']}>
<SearchBox
focusShortcuts={[83, 191]}
onChange={this.handleChange}
onFocus={this.handleFocus}
onSubmit={this.handleSearch}
showLoadingIndicator={false}
translations={{ placeholder }}
/>
</ObserveKeys>
{isDropdownEnabled && isSearchFocused && (
<SearchHits
handleHits={this.handleHits}
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
selectedIndex={index}
/>
)}
</div>
</HotKeys>
</div>
</WithInstantSearch>
);
}
}

View File

@ -46,7 +46,7 @@ const CustomHits = connectHits(
return (
<div className='ais-Hits'>
<ul className='ais-Hits-list'>
<ul className='ais-Hits-list' data-cy='ais-Hits-list'>
{allHits.map((hit, i) => (
<li
className={

View File

@ -0,0 +1,59 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Magnifier from '../../../assets/icons/Magnifier';
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
const SearchBarOptimized = (): JSX.Element => {
const { t } = useTranslation();
const placeholder = t('search.placeholder');
const searchUrl: string = searchPageUrl as string;
const [value, setValue] = useState('');
const onChange = (event: React.ChangeEvent<HTMLInputElement>) =>
setValue(event.target.value);
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (value && value.length > 1) {
window.open(`${searchUrl}?query=${encodeURIComponent(value)}`, '_blank');
}
};
return (
<div className='fcc_searchBar' data-testid='fcc_searchBar'>
<div className='fcc_search_wrapper'>
<div className='ais-SearchBox' data-cy='ais-SearchBox'>
<form
action=''
className='ais-SearchBox-form'
data-cy='ais-SearchBox-form'
onSubmit={onSubmit}
role='search'
>
<input
aria-label='Search'
autoCapitalize='off'
autoComplete='off'
autoCorrect='off'
className='ais-SearchBox-input'
maxLength={512}
onChange={onChange}
placeholder={placeholder}
spellCheck='false'
type='search'
value={value}
/>
<button
className='ais-SearchBox-submit'
title='Submit your search query.'
type='submit'
>
<Magnifier />
</button>
</form>
</div>
</div>
</div>
);
};
SearchBarOptimized.displayName = 'SearchBarOptimized';
export default SearchBarOptimized;

View File

@ -1,56 +0,0 @@
import React from 'react';
import {
Highlight,
connectStateResults,
connectAutoComplete
} from 'react-instantsearch-dom';
import { isEmpty } from 'lodash-es';
import EmptySearch from './EmptySearch';
import NoResults from './NoResults';
import './search-page-hits.css';
const AllHits = connectAutoComplete(({ hits, currentRefinement }) => {
const isHitsEmpty = !hits.length;
return currentRefinement && !isHitsEmpty ? (
<div className='ais-Hits search-page'>
<ul className='ais-Hits-list'>
{hits.map(result => (
<a
href={result.url}
key={result.objectID}
rel='noopener noreferrer'
target='_blank'
>
<li className='ais-Hits-item dataset-node'>
<p>
<Highlight attribute='title' hit={result} />
</p>
</li>
</a>
))}
</ul>
</div>
) : (
<NoResults query={currentRefinement} />
);
});
AllHits.displayName = 'AllHits';
const SearchHits = connectStateResults(({ handleClick, searchState }) => {
const isSearchEmpty = isEmpty(searchState) || isEmpty(searchState.query);
return isSearchEmpty ? (
<EmptySearch />
) : (
<AllHits handleClick={handleClick} />
);
});
SearchHits.displayName = 'SearchHits';
export default SearchHits;

View File

@ -1,48 +0,0 @@
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { Index } from 'react-instantsearch-dom';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
import { withTranslation } from 'react-i18next';
import { updateSearchQuery } from '../components/search/redux';
import SearchPageHits from '../components/search/searchPage/SearchPageHits';
import './search.css';
const propTypes = {
t: PropTypes.func.isRequired,
updateSearchQuery: PropTypes.func.isRequired
};
const mapDispatchToProps = { updateSearchQuery };
class SearchPage extends Component {
componentWillUnmount() {
this.props.updateSearchQuery('');
}
render() {
const { t } = this.props;
return (
<Fragment>
<Helmet title={`${t('search.label')} | freeCodeCamp.org`} />
<Index indexName='news' />
<Grid>
<Row>
<Col xs={12}>
<main>
<SearchPageHits />
</main>
</Col>
</Row>
</Grid>
</Fragment>
);
}
}
SearchPage.displayName = 'SearchPage';
SearchPage.propTypes = propTypes;
export default connect(null, mapDispatchToProps)(withTranslation()(SearchPage));

View File

@ -0,0 +1,66 @@
/* global describe it expect */
import { isChallenge, isLanding } from './path-parsers';
const pathnames = {
english: {
landing: '/',
superBlock: '/learn/responsive-web-design',
challenge:
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements'
},
espanol: {
landing: '/espanol',
superBlock: '/espanol/learn/responsive-web-design',
challenge:
'/espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements'
}
};
describe('isLanding', () => {
it('returns a booleans', () => {
expect(typeof isLanding('/')).toBe('boolean');
});
it('return true for Espanol landing pathname', () => {
expect(isLanding(pathnames.espanol.landing)).toBe(true);
});
it('returns false for Espanol super block pathname', () => {
expect(isLanding(pathnames.espanol.superBlock)).toBe(false);
});
it('returns false for Espanol challenge pathname', () => {
expect(isLanding(pathnames.espanol.challenge)).toBe(false);
});
it('returns true for English landing pathname', () => {
expect(isLanding(pathnames.english.landing)).toBe(true);
});
it('returns false for English super block pathname', () => {
expect(isLanding(pathnames.english.superBlock)).toBe(false);
});
it('returns false for English challenge pathname', () => {
expect(isLanding(pathnames.english.challenge)).toBe(false);
});
});
describe('isChallenge', () => {
it('returns a boolean', () => {
expect(typeof isChallenge('/')).toBe('boolean');
});
it('returns false for Espanol landing pathname', () => {
expect(isChallenge(pathnames.espanol.landing)).toBe(false);
});
it('returns false for Espanol super block pathname', () => {
expect(isChallenge(pathnames.espanol.superBlock)).toBe(false);
});
it('returns true for Espanol challenge pathname', () => {
expect(isChallenge(pathnames.espanol.challenge)).toBe(true);
});
it('returns false for English landing pathname', () => {
expect(isChallenge(pathnames.english.landing)).toBe(false);
});
it('returns false for English super block pathname', () => {
expect(isChallenge(pathnames.english.superBlock)).toBe(false);
});
it('returns true for English challenge pathname', () => {
expect(isChallenge(pathnames.english.challenge)).toBe(true);
});
});

View File

@ -0,0 +1,23 @@
import { i18nConstants } from '../../../config/constants';
const splitPath = (pathname: string): string[] =>
pathname.split('/').filter(x => x);
export const isChallenge = (pathname: string): boolean => {
const pathArray = splitPath(pathname);
return (
(pathArray.length === 4 && pathArray[0]) === 'learn' ||
(pathArray.length === 5 && pathArray[1]) === 'learn'
);
};
export const isLanding = (pathname: string): boolean => {
const pathArray = splitPath(pathname);
const isEnglishLanding = pathArray.length === 0;
const isI18Landing =
pathArray.length === 1 && i18nConstants.includes(pathArray[0]);
return isEnglishLanding || isI18Landing;
};
const pathParsers = { isLanding, isChallenge };
export default pathParsers;

View File

@ -6,6 +6,7 @@ import {
DefaultLayout
} from '../../src/components/layouts';
import FourOhFourPage from '../../src/pages/404';
import { isChallenge } from '../../src/utils/path-parsers';
export default function layoutSelector({ element, props }) {
const {
@ -18,32 +19,23 @@ export default function layoutSelector({ element, props }) {
{element}
</DefaultLayout>
);
}
if (/\/certification\//.test(pathname)) {
} else if (/\/certification\//.test(pathname)) {
return (
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
);
}
const splitPath = pathname.split('/').filter(x => x);
const isChallenge =
(splitPath.length === 4 && splitPath[0]) === 'learn' ||
(splitPath.length === 5 && splitPath[1]) === 'learn';
if (isChallenge) {
} else if (isChallenge(pathname)) {
return (
<DefaultLayout pathname={pathname} showFooter={false}>
{element}
</DefaultLayout>
);
} else {
return (
<DefaultLayout pathname={pathname} showFooter={true}>
{element}
</DefaultLayout>
);
}
return (
<DefaultLayout pathname={pathname} showFooter={true}>
{element}
</DefaultLayout>
);
}
layoutSelector.propTypes = {

View File

@ -4,8 +4,44 @@ for (let i = 0; i < 26; i++) {
alphabet = alphabet.concat(String.fromCharCode(97 + i));
}
const i18nConstants = [
// reserved paths for localizations
'afrikaans',
'arabic',
'bengali',
'catalan',
'chinese',
'czech',
'danish',
'dutch',
'espanol',
'finnish',
'french',
'german',
'greek',
'hebrew',
'hindi',
'hungarian',
'italian',
'japanese',
'korean',
'norwegian',
'polish',
'portuguese',
'romanian',
'russian',
'serbian',
'spanish',
'swahili',
'swedish',
'turkish',
'ukrainian',
'vietnamese'
];
let blocklist = [
...alphabet.split(''),
...i18nConstants,
'about',
'academic-honesty',
'account',
@ -85,40 +121,6 @@ let blocklist = [
'user',
'username',
'wiki',
// reserved paths for localizations
'afrikaans',
'arabic',
'bengali',
'catalan',
'chinese',
'czech',
'danish',
'dutch',
'espanol',
'finnish',
'french',
'german',
'greek',
'hebrew',
'hindi',
'hungarian',
'italian',
'japanese',
'korean',
'norwegian',
'polish',
'portuguese',
'romanian',
'russian',
'serbian',
'spanish',
'swahili',
'swedish',
'turkish',
'ukrainian',
'vietnamese',
// some more names from https://github.com/marteinn/The-Big-Username-Blacklist-JS/blob/master/src/list.js
'.htaccess',
'.htpasswd',
@ -647,4 +649,5 @@ let blocklist = [
'zlib'
];
export const blocklistedUsernames = [...new Set(blocklist)];
exports.blocklistedUsernames = [...new Set(blocklist)];
exports.i18nConstants = i18nConstants;

View File

@ -14,7 +14,7 @@ const clear = () => {
});
};
describe('Search bar', () => {
describe('Search bar optimized', () => {
before(() => {
cy.visit('/');
});
@ -23,6 +23,38 @@ describe('Search bar', () => {
clear();
});
it('Should render properly', () => {
cy.get('[data-cy=ais-SearchBox]').should('be.visible');
});
it('Should not display hits', () => {
search('freeCodeCamp');
cy.get('[data-cy=ais-Hits-list]').should('not.exist');
});
it('Should open a new tab ', () => {
cy.visit('/', {
onBeforeLoad(win) {
cy.stub(win, 'open').as('open');
}
});
search('freeCodeCamp');
cy.get('[data-cy=ais-SearchBox-form]').submit();
const queryUrl =
'https://www.freecodecamp.org/news/search/?query=freeCodeCamp';
cy.get('@open').should('have.been.calledOnceWith', queryUrl);
});
});
describe('Search bar', () => {
before(() => {
cy.visit('/learn');
});
beforeEach(() => {
clear();
});
it('Should render properly', () => {
cy.get('.ais-SearchBox').should('be.visible');
});
@ -30,13 +62,17 @@ describe('Search bar', () => {
it('Should accept input and display hits', () => {
search('freeCodeCamp');
cy.get('.ais-Hits-list').children().should('to.have.length.of.at.least', 1);
cy.get('[data-cy=ais-Hits-list]')
.children()
.should('to.have.length.of.at.least', 1);
});
it('Should clear hits when input is cleared', () => {
search('freeCodeCamp');
cy.get('.ais-Hits-list').children().should('to.have.length.of.at.least', 1);
cy.get('[data-cy=ais-Hits-list]')
.children()
.should('to.have.length.of.at.least', 1);
clear();
@ -48,7 +84,7 @@ describe('Search bar', () => {
search('freeCodeCamp');
cy.get('.ais-Hits-list').children().should('to.have.length.of', 8);
cy.get('[data-cy=ais-Hits-list]').children().should('to.have.length.of', 8);
});
it('Should show up to 5 hits when height < 768px', () => {
@ -56,13 +92,13 @@ describe('Search bar', () => {
search('freeCodeCamp');
cy.get('.ais-Hits-list').children().should('to.have.length.of', 5);
cy.get('[data-cy=ais-Hits-list]').children().should('to.have.length.of', 5);
});
it('Should show no hits for queries that do not exist in the Algolia index', () => {
search('testtttt');
cy.get('.ais-Hits-list').children().should('to.have.length.of', 0);
cy.get('[data-cy=ais-Hits-list]').children().should('to.have.length.of', 0);
cy.contains('No tutorials found');
});

View File

@ -54,8 +54,7 @@ describe('Learn Landing page (not logged in)', () => {
describe('Quotes', () => {
beforeEach(() => {
cy.visit('/');
cy.contains("Get started (it's free)").click();
cy.login();
});
it('Should show a quote', () => {
@ -73,8 +72,7 @@ describe('Quotes', () => {
describe('Superblocks and Blocks', () => {
beforeEach(() => {
cy.visit('/');
cy.contains("Get started (it's free)").click();
cy.login();
});
it('Has all superblocks visible', () => {

View File

@ -2,8 +2,7 @@
describe('Settings', () => {
it('should be possible to reset your progress', () => {
cy.visit('/');
cy.contains("Get started (it's free)").click();
cy.login();
cy.visit('/settings');
cy.contains('Reset all of my progress').click();
cy.contains('Reset everything. I want to start from the beginning').click();

View File

@ -2,8 +2,7 @@
describe('Username input field', () => {
beforeEach(() => {
cy.visit('/');
cy.contains("Get started (it's free)").click({ force: true });
cy.login();
cy.visit('/settings');
// Setting aliases here

View File

@ -35,7 +35,8 @@
Cypress.Commands.add('login', () => {
cy.visit('/');
cy.contains("Get started (it's free)").click({ force: true });
cy.contains("Get started (it's free)").click();
cy.contains('Welcome back');
});
Cypress.Commands.add('resetUsername', () => {