|
| 1 | +import React, { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| 2 | +import ReactDOM from 'react-dom'; |
| 3 | +import Icon from '@ably/ui/core/Icon'; |
| 4 | +import { products } from '@ably/ui/core/ProductTile/data'; |
| 5 | +import ExamplesCheckbox from './ExamplesCheckbox'; |
| 6 | +import examples from '../../data/examples'; |
| 7 | +import Button from '@ably/ui/core/Button'; |
| 8 | +import cn from '@ably/ui/core/utils/cn'; |
| 9 | +import { useOnClickOutside } from 'src/hooks'; |
| 10 | +import Badge from '@ably/ui/core/Badge'; |
| 11 | +import { SelectedFilters } from './ExamplesContent'; |
| 12 | + |
| 13 | +const ExamplesFilter = ({ |
| 14 | + selected, |
| 15 | + setSelected, |
| 16 | + handleSearch, |
| 17 | +}: { |
| 18 | + selected: SelectedFilters; |
| 19 | + setSelected: Dispatch<SetStateAction<SelectedFilters>>; |
| 20 | + handleSearch: (e: ChangeEvent<HTMLInputElement>) => void; |
| 21 | +}) => { |
| 22 | + const filterMenuRef = useRef<HTMLDivElement>(null); |
| 23 | + const [expandFilterMenu, setExpandFilterMenu] = useState(false); |
| 24 | + const [localSelected, setLocalSelected] = useState<SelectedFilters>(selected); |
| 25 | + |
| 26 | + const handleSelect = useCallback((e: ChangeEvent<HTMLInputElement>, filterType: keyof SelectedFilters) => { |
| 27 | + setLocalSelected((prevSelected) => { |
| 28 | + if (e.target.value === 'all') { |
| 29 | + return { |
| 30 | + ...prevSelected, |
| 31 | + [filterType]: [], |
| 32 | + }; |
| 33 | + } |
| 34 | + |
| 35 | + const newSelected = prevSelected[filterType].includes(e.target.value) |
| 36 | + ? prevSelected[filterType].filter((item) => item !== e.target.value) |
| 37 | + : [...prevSelected[filterType], e.target.value]; |
| 38 | + |
| 39 | + return { |
| 40 | + ...prevSelected, |
| 41 | + [filterType]: Array.from(new Set(newSelected)), |
| 42 | + }; |
| 43 | + }); |
| 44 | + }, []); |
| 45 | + |
| 46 | + const filters = useMemo( |
| 47 | + () => [ |
| 48 | + { |
| 49 | + key: 'product', |
| 50 | + data: products, |
| 51 | + selected: localSelected.products, |
| 52 | + handleSelect: (e: ChangeEvent<HTMLInputElement>) => handleSelect(e, 'products'), |
| 53 | + }, |
| 54 | + { |
| 55 | + key: 'use-case', |
| 56 | + data: examples.useCases, |
| 57 | + selected: localSelected.useCases, |
| 58 | + handleSelect: (e: ChangeEvent<HTMLInputElement>) => handleSelect(e, 'useCases'), |
| 59 | + }, |
| 60 | + ], |
| 61 | + [localSelected.products, localSelected.useCases, handleSelect], |
| 62 | + ); |
| 63 | + |
| 64 | + const closeFilterMenu = useCallback(() => { |
| 65 | + setExpandFilterMenu(false); |
| 66 | + setLocalSelected(selected); |
| 67 | + }, [selected]); |
| 68 | + |
| 69 | + useOnClickOutside(closeFilterMenu, filterMenuRef); |
| 70 | + |
| 71 | + useEffect(() => { |
| 72 | + const handleResize = () => { |
| 73 | + if (window.innerWidth >= 1040) { |
| 74 | + setExpandFilterMenu(false); |
| 75 | + } |
| 76 | + }; |
| 77 | + |
| 78 | + window.addEventListener('resize', handleResize); |
| 79 | + return () => window.removeEventListener('resize', handleResize); |
| 80 | + }, []); |
| 81 | + |
| 82 | + useEffect(() => { |
| 83 | + if (window.innerWidth >= 1040) { |
| 84 | + setSelected(localSelected); |
| 85 | + } |
| 86 | + }, [expandFilterMenu, localSelected, setSelected]); |
| 87 | + |
| 88 | + const activeFilters = useMemo( |
| 89 | + () => selected.products.length + selected.useCases.length, |
| 90 | + [selected.products, selected.useCases], |
| 91 | + ); |
| 92 | + |
| 93 | + const handleApply = () => { |
| 94 | + setSelected(localSelected); |
| 95 | + setExpandFilterMenu(false); |
| 96 | + }; |
| 97 | + |
| 98 | + return ( |
| 99 | + <> |
| 100 | + <div className="h-[34px] sm:h-[30px] w-20 absolute left-8 top-4 flex items-center justify-center select-none cursor-default"> |
| 101 | + <Icon name={'icon-gui-magnifying-glass-outline'} size="1rem" /> |
| 102 | + </div> |
| 103 | + <input |
| 104 | + type="search" |
| 105 | + className="ui-input pl-36 w-full h-40 sm:h-34 ui-text-p3" |
| 106 | + placeholder="Find an example" |
| 107 | + autoComplete="off" |
| 108 | + aria-label="Search examples" |
| 109 | + role="searchbox" |
| 110 | + onChange={(e) => handleSearch(e)} |
| 111 | + /> |
| 112 | + <Button |
| 113 | + className="flex sm:hidden mt-16 w-full" |
| 114 | + variant="secondary" |
| 115 | + leftIcon="icon-gui-adjustments-horizontal-outline" |
| 116 | + onClick={() => setExpandFilterMenu(true)} |
| 117 | + > |
| 118 | + Filter |
| 119 | + {activeFilters > 0 ? <Badge>{activeFilters}</Badge> : null} |
| 120 | + </Button> |
| 121 | + {expandFilterMenu && |
| 122 | + ReactDOM.createPortal( |
| 123 | + <div className="fixed inset-0 bg-neutral-1300 opacity-10 z-20" onClick={() => setExpandFilterMenu(false)} />, |
| 124 | + document.body, |
| 125 | + )} |
| 126 | + <div |
| 127 | + ref={filterMenuRef} |
| 128 | + className={cn( |
| 129 | + 'fixed bottom-0 bg-white dark:bg-neutral-1300 z-30 w-full left-0 gap-20 mt-20 translate-y-full sm:static sm:translate-y-0 sm:flex sm:flex-col sm:bg-transparent sm:z-0 transition-[transform,colors] sm:transition-colors rounded-t-2xl sm:rounded-none max-h-[calc(100%-64px)] overflow-y-scroll', |
| 130 | + { |
| 131 | + 'translate-y-0': expandFilterMenu, |
| 132 | + }, |
| 133 | + )} |
| 134 | + > |
| 135 | + <div className="flex justify-between items-center sm:hidden h-64 px-16 py-8 bg-neutral-000 dark:bg-neutral-1300 border border-neutral-300 dark:border-neutral-1000 rounded-t-2xl sm:rounded-none"> |
| 136 | + <p className="ui-text-p1 font-bold text-neutral-1300 dark:text-neutral-000">Filters</p> |
| 137 | + <button onClick={closeFilterMenu} aria-label="Close filter menu"> |
| 138 | + <Icon name="icon-gui-x-mark-outline" size="24px" /> |
| 139 | + </button> |
| 140 | + </div> |
| 141 | + {filters.map(({ key, selected, handleSelect, data }) => ( |
| 142 | + <div key={key} className="p-16 pt-24"> |
| 143 | + <p className="ui-text-overline2 font-medium text-neutral-700">{key.replace(/-/g, ' ').toUpperCase()}</p> |
| 144 | + <div className="mt-8 flex ui-text-p4 flex-col gap-8"> |
| 145 | + <ExamplesCheckbox |
| 146 | + label="All" |
| 147 | + name={`${key}-all`} |
| 148 | + value="all" |
| 149 | + disabled={selected.length === 0} |
| 150 | + isChecked={selected.length === 0} |
| 151 | + handleSelect={handleSelect} |
| 152 | + /> |
| 153 | + {Object.entries(data).map(([itemKey, item]) => ( |
| 154 | + <ExamplesCheckbox |
| 155 | + key={itemKey} |
| 156 | + label={item.label} |
| 157 | + name={`${key}-${itemKey}`} |
| 158 | + value={itemKey} |
| 159 | + handleSelect={handleSelect} |
| 160 | + isChecked={selected.includes(itemKey)} |
| 161 | + /> |
| 162 | + ))} |
| 163 | + </div> |
| 164 | + </div> |
| 165 | + ))} |
| 166 | + <div className="sm:hidden p-16 flex gap-12"> |
| 167 | + <Button |
| 168 | + className="w-full flex-1" |
| 169 | + variant="primary" |
| 170 | + disabled={ |
| 171 | + localSelected.products.length === selected.products.length && |
| 172 | + localSelected.useCases.length === selected.useCases.length && |
| 173 | + localSelected.products.every((product) => selected.products.includes(product)) && |
| 174 | + localSelected.useCases.every((useCase) => selected.useCases.includes(useCase)) |
| 175 | + } |
| 176 | + onClick={handleApply} |
| 177 | + > |
| 178 | + Apply |
| 179 | + </Button> |
| 180 | + </div> |
| 181 | + </div> |
| 182 | + </> |
| 183 | + ); |
| 184 | +}; |
| 185 | + |
| 186 | +export default ExamplesFilter; |
0 commit comments