Skip to content

Instantly share code, notes, and snippets.

@g33kChris
Created December 30, 2019 14:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save g33kChris/dbf39fcc5721ff47c57cc725a4ffa21d to your computer and use it in GitHub Desktop.
Save g33kChris/dbf39fcc5721ff47c57cc725a4ffa21d to your computer and use it in GitHub Desktop.
Example Tag related search code
// Wrap the application in the InstantSearch provider so the search context is available within the application component tree
<InstantSearchProvider>
{element}
</InstantSearchProvider>
export interface ApplicationState {
searchQuery: string;
searchFocus: boolean;
}
import React from "react"
import { Highlight, Snippet } from "react-instantsearch-dom"
import { Link } from "gatsby"
import { Root, Item } from './hit-components-styles';
export const PageHit = clickHandler => ({ hit }) => (
<div>
<Link to={hit.slug} onClick={clickHandler}>
<h4>
<Highlight attribute="title" hit={hit} tagName="mark" />
</h4>
</Link>
<Snippet attribute="excerpt" hit={hit} tagName="mark" />
</div>
)
export const PostHit = clickHandler => ({ hit }) => (
<Root>
<Link to={`/blog/` + hit.slug} onClick={clickHandler}>
<Item>
<Highlight attribute="title" hit={hit} tagName="mark" />
</Item>
</Link>
</Root>
)
import { ApplicationState } from './application-state.interface';
export const initialState: ApplicationState = {
searchQuery: '',
searchFocus: false
}
interface InstantSearchProviderProps {
state: ApplicationState;
dispatch: ({ type }: { type: string; payload?: any; }) => void;
children: any
}
function useOnClickOutside(ref, handler) {
React.useEffect(
() => {
const listener = event => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
},
[ref, handler]
);
}
function InstantSearchProvider(props: InstantSearchProviderProps) {
const ref = React.createRef();
const searchClient = React.useMemo(
() =>
algoliasearch(
process.env.GATSBY_ALGOLIA_APP_ID,
process.env.GATSBY_ALGOLIA_SEARCH_KEY
),
[]
)
useOnClickOutside(ref, () => props.dispatch({ type: searchFocusActionTypes.SET_FOCUS, payload: false }))
return (
<>
<InstantSearch
searchClient={searchClient}
indexName={searchIndices[0].name}
onSearchStateChange={({ query }) => props.dispatch({ type: searchActionTypes.SET_QUERY, payload: query })}
root={{ Root, props: { ref } }}
>
{ props.children }
</InstantSearch>
</>
)
}
export default withApplicationState(InstantSearchProvider)
import { ApplicationState } from './application-state.interface';
import { StateAction } from './state-action.interface';
import searchFocusReducer from './reducers/searchFocus.reducer';
import searchReducer from './reducers/search.reducer';
export default function rootReducer(state: ApplicationState, action: StateAction): ApplicationState {
const { searchQuery, searchFocus } = state;
return {
searchQuery: searchReducer(searchQuery, action),
searchFocus: searchFocusReducer(searchFocus, action)
}
}
import { StateAction } from '../state-action.interface';
export const searchFocusActionTypes = {
SET_FOCUS: 'SET_FOCUS'
}
export default function searchFocusReducer(state: boolean = false, action: StateAction): boolean {
switch(action.type) {
case searchFocusActionTypes.SET_FOCUS: {
return action.payload;
}
default:
return state;
}
}
export const searchIndices = [
{ name: `Posts`, title: `Blog Posts`, hitComp: `PostHit` }
];
import { StateAction } from '../state-action.interface';
export const searchActionTypes = {
SET_QUERY: 'SET_QUERY'
}
export default function searchReducer(state: string = '', action: StateAction): string {
switch(action.type) {
case searchActionTypes.SET_QUERY: {
return action.payload;
}
default:
return state;
}
}
import React from "react";
import {
Index,
connectStateResults
} from "react-instantsearch-dom";
import {
HitsWrapper,
PoweredBy,
ResultsHeader,
StatText,
IndicieHeader,
StyledHits as Hits,
SearchControl,
SearchInput,
CancelButton
} from "./search-styles";
import Input from "../input";
import * as hitComponents from "../hit-components";
import { searchIndices } from '../../../providers/search-indices.const';
import { ApplicationState } from "../../../providers/application-state.interface";
import { searchFocusActionTypes } from '../../../providers/reducers/searchFocus.reducer';
import withApplicationState from "../../_hocs/with-application-state";
const Results = connectStateResults(
({ searchState: state, searchResults: res, children }) =>
res && res.nbHits > 0 ? children : <StatText>{ `No results for '${state.query}'`}</StatText>
)
const Stats = connectStateResults(
({ searchResults: res }) =>
res && res.nbHits > 0 && `${res.nbHits} result${res.nbHits > 1 ? `s` : ``}`
)
interface SearchProps {
collapse: any;
hitsAsGrid: any;
cancelSearch: any;
state: ApplicationState;
dispatch: ({ type }: { type: string; payload?: any; }) => void;
}
const Search = (props: SearchProps) => {
const { collapse, hitsAsGrid, cancelSearch, state, dispatch } = props;
return(
<>
<SearchControl>
<SearchInput>
<Input
onFocus={() => dispatch({ type: searchFocusActionTypes.SET_FOCUS, payload: true })}
{...{ collapse, focus: state.searchFocus }}
/>
</SearchInput>
<CancelButton onClick={() => cancelSearch()}>Cancel</CancelButton>
</SearchControl>
<HitsWrapper show={state.searchQuery.length > 0 } asGrid={hitsAsGrid}>
{ searchIndices.map(({ name, title, hitComp }) => (
<Index key={name} indexName={name}>
<IndicieHeader>
<ResultsHeader>{title}</ResultsHeader>
<StatText><Stats /></StatText>
</IndicieHeader>
<Results>
<Hits hitComponent={hitComponents[hitComp](() => dispatch({ type: searchFocusActionTypes.SET_FOCUS, payload: false }))} />
</Results>
</Index>
))}
<PoweredBy />
</HitsWrapper>
</>
);
};
export default withApplicationState(Search);
interface StateProviderProps {
children: any;
}
interface IStateContext {
state: ApplicationState;
dispatch: ({type}:{type:string}) => void;
}
export const GlobalStore = React.createContext({} as IStateContext);
const asyncer = (dispatch: any, state: ApplicationState) => (action: any) =>
typeof action === 'function' ? action(dispatch, state) : dispatch(action);
export function StateProvider(props: StateProviderProps) {
const [state, dispatchBase] = React.useReducer(rootReducer, initialState);
const dispatch = React.useCallback(asyncer(dispatchBase, state), [])
return (
<GlobalStore.Provider value={{ state, dispatch }}>
{ props.children }
</GlobalStore.Provider>
)
}
import * as React from 'react';
import { ApplicationState } from '../../../providers/application-state.interface';
import { isExpandedActionTypes } from '../../../providers/reducers/is-expanded.reducer';
import { userModeActionTypes } from '../../../providers/reducers/user-mode.reducer';
import { Tags as Root, Tag as Item } from '../../primitives';
import withApplicationState from '../../_hocs/with-application-state';
import * as React from 'react';
import { ApplicationState } from '../../../providers/application-state.interface';
import { isExpandedActionTypes } from '../../../providers/reducers/is-expanded.reducer';
import { userModeActionTypes } from '../../../providers/reducers/user-mode.reducer';
import { Tags as Root, Tag as Item } from '../../primitives';
import withApplicationState from '../../_hocs/with-application-state';
import { connectSearchBox } from 'react-instantsearch-dom';
interface TagsProps {
refine: any,
tags: string[]
state: ApplicationState;
dispatch: any;
}
function Tags(props: TagsProps) {
function search(tag: string) {
props.dispatch({ type: isExpandedActionTypes.EXPAND });
props.dispatch({ type: userModeActionTypes.SET_USER_MODE, payload: 'search' });
props.dispatch(
() => setTimeout(() => props.refine(tag), 300)
);
props.refine(tag);
}
return (
<Root>
{
props.tags.map(t => (
<Item>
<button onClick={() => search(t)}>{ t }</button>
</Item>
))
}
</Root>
)
}
export default connectSearchBox(withApplicationState(Tags));
import { GlobalStore } from '../../providers/state.provider';
export default function withApplicationState(Component: any) {
return function WrapperComponent(props: any) {
return (
<GlobalStore.Consumer>
{context => <Component {...props} state={context.state} dispatch={context.dispatch} />}
</GlobalStore.Consumer>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment