Skip to content
This repository has been archived by the owner on Jun 11, 2021. It is now read-only.

Commit

Permalink
feat(docsearch): add recent searches (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Apr 2, 2020
1 parent ba6bdde commit 4c0aa3b
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 123 deletions.
66 changes: 43 additions & 23 deletions src/DocSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import {
} from '@francoischalifour/autocomplete-core';
import { getAlgoliaHits } from '@francoischalifour/autocomplete-preset-algolia';

import { DocSearchHit, InternalDocSearchHit } from './types';
import {
DocSearchHit,
InternalDocSearchHit,
RecentDocSearchHit,
} from './types';
import { createSearchClient, groupBy, noop } from './utils';
import { SearchBox } from './SearchBox';
import { Dropdown } from './Dropdown';
import { Footer } from './Footer';

import { createRecentSearches } from './recent-searches';

interface DocSearchProps {
appId?: string;
apiKey: string;
Expand Down Expand Up @@ -42,18 +48,9 @@ export function DocSearch({
appId,
apiKey,
]);
const recentSearches = useRef(createRecentSearches<RecentDocSearchHit>());

const {
getEnvironmentProps,
getRootProps,
getFormProps,
getLabelProps,
getInputProps,
getMenuProps,
getItemProps,
setQuery,
refresh,
} = React.useMemo(
const autocomplete = React.useMemo(
() =>
createAutocomplete<
InternalDocSearchHit,
Expand Down Expand Up @@ -127,9 +124,27 @@ export function DocSearch({
});
}

if (!query) {
return [
{
onSelect({ suggestion }) {
recentSearches.current.saveSearch(suggestion);
onClose();
},
getSuggestionUrl({ suggestion }) {
return suggestion.url;
},
getSuggestions() {
return recentSearches.current.getSearches();
},
},
];
}

return Object.values<DocSearchHit[]>(sources).map(items => {
return {
onSelect() {
onSelect({ suggestion }) {
recentSearches.current.saveSearch(suggestion);
onClose();
},
getSuggestionUrl({ suggestion }) {
Expand Down Expand Up @@ -165,6 +180,8 @@ export function DocSearch({
[indexName, searchParameters, searchClient, onClose]
);

const { getEnvironmentProps, getRootProps } = autocomplete;

useEffect(() => {
const isMobileMediaQuery = window.matchMedia('(max-width: 750px)');

Expand Down Expand Up @@ -219,23 +236,26 @@ export function DocSearch({
<div className="DocSearch-Modal">
<header className="DocSearch-SearchBar" ref={searchBoxRef}>
<SearchBox
inputRef={inputRef}
query={state.query}
getFormProps={getFormProps}
getLabelProps={getLabelProps}
getInputProps={getInputProps}
{...autocomplete}
state={state}
onClose={onClose}
inputRef={inputRef}
/>
</header>

<div className="DocSearch-Dropdown" ref={dropdownRef}>
<Dropdown
inputRef={inputRef}
{...autocomplete}
state={state}
getMenuProps={getMenuProps}
getItemProps={getItemProps}
setQuery={setQuery}
refresh={refresh}
inputRef={inputRef}
onItemClick={item => {
recentSearches.current.saveSearch(item);
onClose();
}}
onAction={item => {
recentSearches.current.deleteSearch(item);
autocomplete.refresh();
}}
/>
</div>

Expand Down
59 changes: 30 additions & 29 deletions src/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,49 @@
import React from 'react';
import {
AutocompleteApi,
AutocompleteState,
GetMenuProps,
GetItemProps,
} from '@francoischalifour/autocomplete-core';

import { InternalDocSearchHit } from '../types';
import { Error } from '../Error';
import { NoResults } from '../NoResults';
import { InternalDocSearchHit, RecentDocSearchHit } from '../types';
import { EmptyScreen } from '../EmptyScreen';
import { Results } from '../Results';
import { NoResults } from '../NoResults';
import { Error } from '../Error';

interface DropdownProps {
state: AutocompleteState<InternalDocSearchHit>;
getMenuProps: GetMenuProps;
getItemProps: GetItemProps<InternalDocSearchHit, React.MouseEvent>;
setQuery(value: string): void;
refresh(): Promise<void>;
inputRef: React.MutableRefObject<HTMLInputElement>;
interface DropdownProps<TItem>
extends AutocompleteApi<
TItem,
React.FormEvent,
React.MouseEvent,
React.KeyboardEvent
> {
state: AutocompleteState<TItem>;
onItemClick(search: RecentDocSearchHit): void;
onAction(search: RecentDocSearchHit): void;
inputRef: React.MutableRefObject<null | HTMLInputElement>;
}

export function Dropdown(props: DropdownProps) {
export function Dropdown(props: DropdownProps<InternalDocSearchHit>) {
if (props.state.status === 'error') {
return <Error />;
}

if (
props.state.status === 'idle' &&
props.state.suggestions.every(source => source.items.length === 0)
) {
const hasSuggestions = props.state.suggestions.some(
source => source.items.length > 0
);

if (!props.state.query) {
return (
<NoResults
setQuery={props.setQuery}
refresh={props.refresh}
state={props.state}
inputRef={props.inputRef}
<EmptyScreen
{...(props as DropdownProps<any>)}
hasSuggestions={hasSuggestions}
/>
);
}

return (
<Results
suggestions={props.state.suggestions}
getMenuProps={props.getMenuProps}
getItemProps={props.getItemProps}
/>
);
if (props.state.status === 'idle' && hasSuggestions === false) {
return <NoResults {...props} />;
}

return <Results {...props} />;
}
147 changes: 147 additions & 0 deletions src/EmptyScreen/EmptyScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import {
AutocompleteApi,
AutocompleteState,
} from '@francoischalifour/autocomplete-core';

import { RecentDocSearchHit } from '../types';

interface EmptyScreenProps
extends AutocompleteApi<
RecentDocSearchHit,
React.FormEvent,
React.MouseEvent,
React.KeyboardEvent
> {
state: AutocompleteState<RecentDocSearchHit>;
hasSuggestions: boolean;
onItemClick(item: RecentDocSearchHit): void;
onAction(search: RecentDocSearchHit): void;
}

export function EmptyScreen(props: EmptyScreenProps) {
if (props.state.status === 'idle' && props.hasSuggestions === false) {
return (
<div>
<p>Select results and your history will appear here.</p>
</div>
);
}

return (
<div className="DocSearch-Dropdown-Container">
{props.state.suggestions.map(({ source, items }, index) => {
return (
<section key={['recent', index].join(':')} className="DocSearch-Hits">
<div className="DocSearch-Hit-source">Recent</div>

<ul {...props.getMenuProps()}>
{items.map(item => {
return (
<li
key={['recent', item.objectID].join(':')}
className="DocSearch-Hit"
{...props.getItemProps({
item,
source,
onClick() {
props.onItemClick(item);
},
})}
>
<a href={item.url}>
<div className="DocSearch-Hit-Container">
<div className="DocSearch-Hit-icon">
<svg width="20" height="20">
<g
stroke="currentColor"
strokeWidth="2"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3.18 6.6a8.23 8.23 0 1112.93 9.94h0a8.23 8.23 0 01-11.63 0" />
<path d="M6.44 7.25H2.55V3.36M10.45 6v5.6M10.45 11.6L13 13" />
</g>
</svg>
</div>

{item.hierarchy[item.type] && item.type === 'lvl1' && (
<div className="DocSearch-Hit-content-wrapper">
<span className="DocSearch-Hit-title">
{item.hierarchy.lvl1}
</span>
{item.content && (
<span className="DocSearch-Hit-path">
{item.content}
</span>
)}
</div>
)}

{item.hierarchy[item.type] &&
(item.type === 'lvl2' ||
item.type === 'lvl3' ||
item.type === 'lvl4' ||
item.type === 'lvl5' ||
item.type === 'lvl6') && (
<div className="DocSearch-Hit-content-wrapper">
<span className="DocSearch-Hit-title">
{item.hierarchy[item.type]}
</span>
<span className="DocSearch-Hit-path">
{item.hierarchy.lvl1}
</span>
</div>
)}

{item.type === 'content' && (
<div className="DocSearch-Hit-content-wrapper">
<span className="DocSearch-Hit-title">
{item.content}
</span>
<span className="DocSearch-Hit-path">
{item.hierarchy.lvl1}
</span>
</div>
)}

<div className="DocSearch-Hit-action">
<button
className="DocSearch-Hit-action-button"
title="Delete this search"
onClick={event => {
event.preventDefault();
event.stopPropagation();

props.onAction(item);
}}
>
<svg width="20" height="20">
<g
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path
d="M10,10 L15.0853291,4.91467086 L10,10 L15.0853291,15.0853291 L10,10 Z M10,10 L4.91467086,4.91467086 L10,10 L4.91467086,15.0853291 L10,10 Z"
transform="translate(10.000000, 10.000000) rotate(-360.000000) translate(-10.000000, -10.000000) "
></path>
</g>
</svg>
</button>
</div>
</div>
</a>
</li>
);
})}
</ul>
</section>
);
})}
</div>
);
}
1 change: 1 addition & 0 deletions src/EmptyScreen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './EmptyScreen';
23 changes: 16 additions & 7 deletions src/NoResults/NoResults.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import React from 'react';
import { AutocompleteState } from '@francoischalifour/autocomplete-core';
import {
AutocompleteApi,
AutocompleteState,
} from '@francoischalifour/autocomplete-core';

interface NoResultsProps {
state: AutocompleteState<any>;
setQuery(value: string): void;
refresh(): Promise<void>;
inputRef: React.MutableRefObject<HTMLInputElement>;
import { InternalDocSearchHit } from '../types';

interface NoResultsProps
extends AutocompleteApi<
InternalDocSearchHit,
React.FormEvent,
React.MouseEvent,
React.KeyboardEvent
> {
state: AutocompleteState<InternalDocSearchHit>;
inputRef: React.MutableRefObject<null | HTMLInputElement>;
}

export function NoResults(props: NoResultsProps) {
Expand All @@ -30,7 +39,7 @@ export function NoResults(props: NoResultsProps) {
onClick={() => {
props.setQuery(search.toLowerCase() + ' ');
props.refresh();
props.inputRef.current.focus();
props.inputRef.current!.focus();
}}
>
{search}
Expand Down
Loading

0 comments on commit 4c0aa3b

Please sign in to comment.