import * as React from 'react'; type Filter = { label: string; key: string; values: any; }; interface FilterContextProps { appliedFilters: Record; searchTerm?: string; selectedSection: string; hideBackButton?: boolean; renderDropdownContent: React.ReactNode | (() => null); applyFilter: (filterKey: string, label: string, values: any) => void; applyFilters: (filters: ({ key: string } & Filter)[]) => void; applySearchTerm: (searchTerm: string) => void; goToFilter: (filterName: string, hideLink?: boolean) => void; goBack: () => void; deleteFilter: (filterKey: string) => void; deleteAllFilters: () => void; getValues: (filterKey: string) => any; getFilters: () => any; getFiltersCount: () => number; isBackButtonHidden: () => boolean; getSearchTerm: () => string; deleteAllFiltersAndSearchTerm: () => void; } const noop = () => {}; export const FilterContext = React.createContext({ appliedFilters: {}, searchTerm: '', selectedSection: '', // Related to routing... might wanna remove this out hideBackButton: false, // Related to routing... might wanna remove this out renderDropdownContent: () => null, applyFilter: noop, applyFilters: noop, applySearchTerm: noop, goToFilter: noop, goBack: noop, deleteFilter: noop, deleteAllFilters: noop, getValues: noop, getFilters: noop, getFiltersCount: () => 0, isBackButtonHidden: () => false, getSearchTerm: () => '', deleteAllFiltersAndSearchTerm: noop, }); export const FilterContextConsumer = FilterContext.Consumer; function formatAppliedFilters(appliedFilters: Record) { return Object.entries(appliedFilters).map(([key, { values, label }]) => ({ key, values, label, })); } const actionTypes = { APPLY_FILTER: 'APPLY_FILTER', APPLY_FILTERS: 'APPLY_FILTERS', APPLY_SEARCH_TERM: 'APPLY_SEARCH_TERM', DELETE_FILTER: 'DELETE_FILTER', DELETE_ALL_FILTERS: 'DELETE_ALL_FILTERS', GO_TO_FILTER: 'GO_TO_FILTER', GO_BACK: 'GO_BACK', DELETE_ALL_FILTERS_AND_SEARCH_TERM: 'DELETE_ALL_FILTERS_AND_SEARCH_TERM', }; function deleteByKey(key: string, object: any) { const nextObject = { ...object }; delete nextObject[key]; return nextObject; } interface ReducerState { appliedFilters: Record; searchTerm: string; selectedSection: string; isBackButtonHidden: boolean; } interface ReducerAction { type: any; payload?: any; } function reducer(state: ReducerState, action: ReducerAction) { switch (action.type) { case actionTypes.APPLY_FILTER: return { ...state, appliedFilters: { ...state.appliedFilters, [action.payload.filterKey]: { label: action.payload.label, values: action.payload.values, }, }, }; case actionTypes.APPLY_FILTERS: return { ...state, appliedFilters: action.payload.filters, }; case actionTypes.APPLY_SEARCH_TERM: return { ...state, searchTerm: action.payload, }; case actionTypes.DELETE_FILTER: return { ...state, appliedFilters: deleteByKey(action.payload, state.appliedFilters), }; case actionTypes.DELETE_ALL_FILTERS: return { ...state, appliedFilters: [], }; case actionTypes.DELETE_ALL_FILTERS_AND_SEARCH_TERM: return { ...state, searchTerm: '', appliedFilters: {}, }; case actionTypes.GO_TO_FILTER: return { ...state, selectedSection: action.payload.selectedSection, isBackButtonHidden: action.payload.isBackButtonHidden, }; case actionTypes.GO_BACK: return { ...state, selectedSection: '', isBackButtonHidden: false, }; default: return state; } } type RenderDropdownContentCb = { closeDropdown: () => void; }; interface FilterPanelProviderProps { initialFilters?: Filter[]; initialSearchTerm?: string; onFilterChange?: (filters: Filter[], searchTerm: string) => void; renderDropdownContent?: React.ReactNode | ((props: RenderDropdownContentCb) => React.ReactNode); } const useFilterPanel = ({ initialFilters, initialSearchTerm, }: Pick) => { return React.useReducer(reducer, { appliedFilters: initialFilters.reduce( (acc, { key, values, label }) => ({ ...acc, [key]: { values, label, }, }), {}, ), searchTerm: initialSearchTerm, selectedSection: '', // related to routing, might want to move this out... isBackButtonHidden: false, // related to routing, might want to move this out... }); }; export const FilterPanelProvider: React.FC = ({ initialFilters, initialSearchTerm, onFilterChange, renderDropdownContent, children, }) => { const [state, dispatch] = useFilterPanel({ initialFilters, initialSearchTerm }); const firstRun = React.useRef(true); React.useEffect(() => { if (onFilterChange && typeof onFilterChange === 'function' && !firstRun.current) { onFilterChange(formatAppliedFilters(state.appliedFilters), state.searchTerm); } if (firstRun.current) { firstRun.current = false; } }, [state.appliedFilters, state.searchTerm]); return ( dispatch({ type: actionTypes.APPLY_FILTER, payload: { filterKey, label, values } }), applyFilters: filters => { const formattedFilters = filters.reduce( (acc, { key, values, label }) => ({ ...acc, [key]: { values, label, }, }), {}, ); dispatch({ type: actionTypes.APPLY_FILTERS, payload: { filters: formattedFilters } }); }, applySearchTerm: searchTerm => dispatch({ type: actionTypes.APPLY_SEARCH_TERM, payload: searchTerm }), goToFilter: (selectedSection, isBackButtonHidden = false) => dispatch({ type: actionTypes.GO_TO_FILTER, payload: { selectedSection, isBackButtonHidden }, }), goBack: () => dispatch({ type: actionTypes.GO_BACK }), deleteFilter: filterKey => dispatch({ type: actionTypes.DELETE_FILTER, payload: filterKey }), deleteAllFilters: () => dispatch({ type: actionTypes.DELETE_ALL_FILTERS }), getValues: filterKey => state.appliedFilters[filterKey] && state.appliedFilters[filterKey].values, getFilters: () => formatAppliedFilters(state.appliedFilters), getFiltersCount: () => Object.keys(state.appliedFilters).length, isBackButtonHidden: () => state.isBackButtonHidden, getSearchTerm: () => state.searchTerm, renderDropdownContent, deleteAllFiltersAndSearchTerm: () => dispatch({ type: actionTypes.DELETE_ALL_FILTERS_AND_SEARCH_TERM }), }} > {children} ); }; FilterPanelProvider.defaultProps = { onFilterChange: () => {}, initialFilters: [], }; export function useFilteringActions(filterKey: string) { const { getValues, applyFilter, deleteFilter } = React.useContext(FilterContext); return { initialValues: getValues(filterKey), applyFilter: (label: string, values: any) => applyFilter(filterKey, label, values), deleteFilter: () => deleteFilter(filterKey), }; } export function useDropdownRouting() { const { goBack, goToFilter, isBackButtonHidden, selectedSection } = React.useContext( FilterContext, ); return { goBack, goToFilter, isBackButtonHidden, selectedSection }; }