-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Navigation component + Page/Layout concept (#42)
This PR adds the navigation component, and also reworks the react-router usage into a Page and Layout system. For the Navigation resizing, I tried to use resize observer, first with https://www.npmjs.com/package/@react-hook/resize-observer and then with ResizeObserver directly. The third party hook did not seem to work well for our use case, and for using ResizeObserver directly, it turns out the TypeScript types for ResizeObserver are not included by default, and I felt a little apprehensive to include third party types for it, though it would probably be fine. microsoft/TypeScript#28502 J=SLAP-1558 TEST=manual test resizing the page and seeing tabs appear and disappear test that switching tabs will run searches
- Loading branch information
Showing
15 changed files
with
450 additions
and
181 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,184 +1,39 @@ | ||
import './sass/App.scss'; | ||
import VerticalSearchPage from './pages/VerticalSearchPage'; | ||
import UniversalSearchPage from './pages/UniversalSearchPage'; | ||
import PageRouter from './PageRouter'; | ||
import StandardLayout from './pages/StandardLayout'; | ||
import { AnswersActionsProvider } from '@yext/answers-headless-react'; | ||
import AlternativeVerticals from './components/AlternativeVerticals'; | ||
import DecoratedAppliedFilters from './components/DecoratedAppliedFilters'; | ||
import { StandardCard } from './components/cards/StandardCard'; | ||
import ResultsCount from './components/ResultsCount'; | ||
import SearchBar from './components/SearchBar'; | ||
import StaticFilters from './components/StaticFilters'; | ||
import VerticalResults from './components/VerticalResults'; | ||
import SpellCheck from './components/SpellCheck'; | ||
import LocationBias from './components/LocationBias'; | ||
import UniversalResults from './components/UniversalResults'; | ||
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; | ||
import Facets from './components/Facets'; | ||
|
||
function App() { | ||
const staticFilterOptions = [ | ||
{ | ||
label: 'canada', | ||
fieldId: 'c_employeeCountry', | ||
value: 'Canada', | ||
}, | ||
{ | ||
label: 'remote', | ||
fieldId: 'c_employeeCountry', | ||
value: 'Remote' | ||
}, | ||
{ | ||
label: 'usa', | ||
fieldId: 'c_employeeCountry', | ||
value: 'United States', | ||
}, | ||
{ | ||
label: 'tech', | ||
fieldId: 'c_employeeDepartment', | ||
value: 'Technology' | ||
}, | ||
{ | ||
label: 'consult', | ||
fieldId: 'c_employeeDepartment', | ||
value: 'Consulting', | ||
}, | ||
{ | ||
label: 'fin', | ||
fieldId: 'c_employeeDepartment', | ||
value: 'Finance', | ||
} | ||
] | ||
|
||
const universalResultsConfig = { | ||
people: { | ||
label: "People", | ||
viewMore: true, | ||
cardConfig: { | ||
CardComponent: StandardCard, | ||
showOrdinal: true | ||
} | ||
}, | ||
events: { | ||
label: "events", | ||
cardConfig: { | ||
CardComponent: StandardCard, | ||
showOrdinal: true | ||
} | ||
}, | ||
links: { | ||
label: "links", | ||
viewMore: true, | ||
cardConfig: { | ||
CardComponent: StandardCard, | ||
showOrdinal: true | ||
} | ||
}, | ||
financial_professionals: { | ||
label: "Financial Professionals", | ||
}, | ||
healthcare_professionals: { | ||
label: "Healthcare Professionals", | ||
import { universalResultsConfig } from './universalResultsConfig'; | ||
|
||
const routes = [ | ||
{ | ||
path: '/', | ||
exact: true, | ||
page: <UniversalSearchPage universalResultsConfig={universalResultsConfig} /> | ||
}, | ||
...Object.keys(universalResultsConfig).map(key => { | ||
return { | ||
path: `/${key}`, | ||
page: <VerticalSearchPage verticalKey={key} /> | ||
} | ||
} | ||
|
||
const universalResultsFilterConfig = { | ||
show: true | ||
}; | ||
|
||
const facetConfigs = { | ||
c_employeeDepartment: { | ||
label: 'Employee Department!' | ||
} | ||
} | ||
}) | ||
]; | ||
|
||
export default function App() { | ||
return ( | ||
<AnswersActionsProvider | ||
apiKey='2d8c550071a64ea23e263118a2b0680b' | ||
experienceKey='slanswers' | ||
locale='en' | ||
verticalKey='people' | ||
> | ||
{/* | ||
TODO: use Navigation component for routing when that's added to repo. | ||
current setup is for testing purposes. | ||
*/} | ||
<Router> | ||
<Switch> | ||
{/* universal search */} | ||
<Route exact path='/'> | ||
<div className='start'> | ||
test | ||
</div> | ||
<div className='end'> | ||
<SearchBar | ||
placeholder='Search...' | ||
isVertical={false} | ||
/> | ||
<div> | ||
<UniversalResults | ||
appliedFiltersConfig={universalResultsFilterConfig} | ||
verticalConfigs={universalResultsConfig} | ||
/> | ||
</div> | ||
</div> | ||
</Route> | ||
|
||
{/* vertical page */} | ||
<Route path={Object.keys(universalResultsConfig).map(key => `/${key}`)}> | ||
<div> | ||
A VERTICAL PAGE! | ||
</div> | ||
</Route> | ||
|
||
{/* vertical search */} | ||
<Route exact path='/vertical'> | ||
<div className='start'> | ||
test | ||
<StaticFilters | ||
title='~Country and Employee Departments~' | ||
options={staticFilterOptions} | ||
/> | ||
<Facets | ||
searchOnChange={true} | ||
searchable={true} | ||
collapsible={true} | ||
defaultExpanded={true} | ||
facetConfigs={facetConfigs} | ||
/> | ||
<SpellCheck | ||
isVertical={true} | ||
/> | ||
</div> | ||
<div className='end'> | ||
<SearchBar | ||
placeholder='Search...' | ||
isVertical={true} | ||
/> | ||
<div> | ||
<ResultsCount /> | ||
<DecoratedAppliedFilters | ||
showFieldNames={true} | ||
hiddenFields={['builtin.entityType']} | ||
delimiter='|' | ||
/> | ||
<AlternativeVerticals | ||
currentVerticalLabel='People' | ||
verticalsConfig={[ | ||
{ label: 'Locations', verticalKey: 'KM' }, | ||
{ label: 'FAQs', verticalKey: 'faq' } | ||
]} | ||
/> | ||
<VerticalResults | ||
CardComponent={StandardCard} | ||
cardConfig={{ showOrdinal: true }} | ||
displayAllResults={true} | ||
/> | ||
<LocationBias isVertical={false} /> | ||
</div> | ||
</div> | ||
</Route> | ||
</Switch> | ||
</Router> | ||
<div className='App'> | ||
<PageRouter | ||
Layout={StandardLayout} | ||
routes={routes} | ||
/> | ||
</div> | ||
</AnswersActionsProvider> | ||
); | ||
} | ||
|
||
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { ComponentType } from 'react'; | ||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; | ||
|
||
interface RouteData { | ||
path: string | ||
page: JSX.Element | ||
exact?: boolean | ||
} | ||
|
||
export type LayoutComponent = ComponentType<{ page: JSX.Element }> | ||
|
||
interface PageProps { | ||
Layout?: LayoutComponent | ||
routes: RouteData[] | ||
} | ||
|
||
/** | ||
* PageRouter abstracts away logic surrounding react-router, and provides an easy way | ||
* to specify a {@link LayoutComponent} for a page. | ||
*/ | ||
export default function PageRouter({ Layout, routes }: PageProps) { | ||
const pages = routes.map(routeData => { | ||
const { path, page, exact } = routeData; | ||
if (Layout) { | ||
return ( | ||
<Route key={path} path={path} exact={exact}> | ||
<Layout page={page}/> | ||
</Route> | ||
); | ||
} | ||
return <Route key={path} path={path} exact={exact}>{page}</Route>; | ||
}); | ||
|
||
return ( | ||
<Router> | ||
<Switch> | ||
{pages} | ||
</Switch> | ||
</Router> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import classNames from 'classnames'; | ||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; | ||
import { NavLink, useLocation } from 'react-router-dom'; | ||
import { ReactComponent as KebabIcon } from '../icons/kebab.svg'; | ||
import '../sass/Navigation.scss'; | ||
|
||
interface LinkData { | ||
to: string | ||
label: string | ||
} | ||
|
||
interface NavigationProps { | ||
links: LinkData[] | ||
} | ||
|
||
export default function Navigation({ links }: NavigationProps) { | ||
// Close the menu when clicking the document | ||
const [menuOpen, setMenuOpen] = useState<boolean>(false); | ||
const menuRef = useRef<HTMLButtonElement>(null); | ||
const handleDocumentClick = (e: MouseEvent) => { | ||
if (e.target !== menuRef.current) { | ||
setMenuOpen(false); | ||
} | ||
}; | ||
useLayoutEffect(() => { | ||
document.addEventListener('click', handleDocumentClick) | ||
return () => document.removeEventListener('click', handleDocumentClick); | ||
}, []); | ||
|
||
// Responsive tabs | ||
const [numOverflowLinks, setNumOverflowLinks] = useState<number>(0); | ||
const navigationRef = useRef<HTMLDivElement>(null); | ||
const handleResize = useCallback(() => { | ||
const navEl = navigationRef.current; | ||
if (!navEl) { | ||
return; | ||
} | ||
const isOverflowing = navEl.scrollWidth > navEl.offsetWidth; | ||
if (isOverflowing && numOverflowLinks < links.length) { | ||
setNumOverflowLinks(numOverflowLinks + 1); | ||
} | ||
}, [links.length, numOverflowLinks]) | ||
useLayoutEffect(handleResize, [handleResize]); | ||
useEffect(() => { | ||
let timeoutId: NodeJS.Timeout; | ||
function resizeListener() { | ||
clearTimeout(timeoutId); | ||
timeoutId = setTimeout(() => { | ||
setNumOverflowLinks(0); | ||
handleResize() | ||
}, 50) | ||
}; | ||
window.addEventListener('resize', resizeListener); | ||
return () => window.removeEventListener('resize', resizeListener); | ||
}, [handleResize]); | ||
|
||
const { search } = useLocation(); | ||
const visibleLinks = links.slice(0, links.length - numOverflowLinks); | ||
const overflowLinks = links.slice(-numOverflowLinks); | ||
const menuButtonClassNames = classNames('Navigation__menuButton', { | ||
'Navigation__menuButton--open': menuOpen | ||
}); | ||
return ( | ||
<nav className='Navigation' ref={navigationRef}> | ||
<div className='Navigation__links'> | ||
{visibleLinks.map(l => renderLink(l, search))} | ||
</div> | ||
{numOverflowLinks > 0 && | ||
<div className='Navigation__menuWrapper'> | ||
<button | ||
className={menuButtonClassNames} | ||
ref={menuRef} | ||
onClick={() => setMenuOpen(!menuOpen)} | ||
> | ||
<KebabIcon /> More | ||
</button> | ||
<div className='Navigation__menuLinks'> | ||
{menuOpen && overflowLinks.map(l => renderLink(l, search))} | ||
</div> | ||
</div> | ||
} | ||
</nav> | ||
) | ||
} | ||
|
||
function renderLink(linkData: LinkData, queryParams: string) { | ||
const { to, label } = linkData; | ||
return ( | ||
<NavLink | ||
key={to} | ||
className='Navigation__link' | ||
activeClassName='Navigation__link--currentRoute' | ||
to={`${to}${queryParams}`} | ||
exact={true} | ||
> | ||
{label} | ||
</NavLink> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.