1import { SearchSmIcon, XIcon } from '@expo/styleguide-icons'; 2import { Command } from 'cmdk'; 3// eslint-disable-next-line lodash/import-scope 4import { groupBy } from 'lodash'; 5import { useEffect, useState } from 'react'; 6import type { Dispatch, SetStateAction } from 'react'; 7 8import { BarLoader } from './BarLoader'; 9import { CommandFooter } from './CommandFooter'; 10import { RNDirectoryItem, RNDocsItem, ExpoDocsItem, ExpoItem } from './Items'; 11import { entries } from './expoEntries'; 12import { searchIconStyle, closeIconStyle } from './styles'; 13import type { ExpoItemType, RNDirectoryItemType, AlgoliaItemType } from './types'; 14import { getExpoDocsResults, getRNDocsResults, getDirectoryResults, getItemsAsync } from './utils'; 15 16import { CALLOUT } from '~/ui/components/Text'; 17 18type Props = { 19 version: string; 20 open: boolean; 21 setOpen: Dispatch<SetStateAction<boolean>>; 22}; 23 24export const CommandMenu = ({ version, open, setOpen }: Props) => { 25 const [initialized, setInitialized] = useState(false); 26 const [loading, setLoading] = useState(false); 27 const [query, setQuery] = useState(''); 28 const [expoDocsItems, setExpoDocsItems] = useState<AlgoliaItemType[]>([]); 29 const [expoItems, setExpoItems] = useState<ExpoItemType[]>([]); 30 const [rnDocsItems, setRnDocsItems] = useState<AlgoliaItemType[]>([]); 31 const [directoryItems, setDirectoryItems] = useState<RNDirectoryItemType[]>([]); 32 33 const getExpoDocsItems = async () => 34 getItemsAsync(query, getExpoDocsResults, setExpoDocsItems, version); 35 const getRNDocsItems = async () => getItemsAsync(query, getRNDocsResults, setRnDocsItems); 36 const getDirectoryItems = async () => 37 getItemsAsync(query, getDirectoryResults, setDirectoryItems); 38 39 const getExpoItems = async () => { 40 setExpoItems(entries.filter(entry => entry.label.toLowerCase().includes(query.toLowerCase()))); 41 }; 42 43 const dismiss = () => setOpen(false); 44 45 const fetchData = (callback: () => void) => { 46 Promise.all([getExpoDocsItems(), getRNDocsItems(), getDirectoryItems(), getExpoItems()]).then( 47 callback 48 ); 49 }; 50 51 const onQueryChange = () => { 52 if (open) { 53 setLoading(true); 54 const inputTimeout = setTimeout(() => fetchData(() => setLoading(false)), 150); 55 return () => clearTimeout(inputTimeout); 56 } 57 }; 58 59 const onMenuOpen = () => { 60 if (open && !initialized) { 61 fetchData(() => { 62 setInitialized(true); 63 setLoading(false); 64 }); 65 } 66 }; 67 68 useEffect(onMenuOpen, [open]); 69 useEffect(onQueryChange, [query]); 70 71 const totalCount = 72 expoDocsItems.length + rnDocsItems.length + directoryItems.length + expoItems.length; 73 74 const expoDocsGroupedItems = groupBy( 75 expoDocsItems.map((expoDocsItem: AlgoliaItemType) => ({ 76 ...expoDocsItem, 77 baseUrl: expoDocsItem.url.replace(/#.+/, ''), 78 })), 79 'baseUrl' 80 ); 81 82 return ( 83 <Command.Dialog open={open} onOpenChange={setOpen} label="Search Menu" shouldFilter={false}> 84 <SearchSmIcon className="text-icon-secondary" css={searchIconStyle} /> 85 <div css={closeIconStyle}> 86 <XIcon className="text-icon-secondary" onClick={() => setOpen(false)} /> 87 </div> 88 <Command.Input value={query} onValueChange={setQuery} placeholder="Search…" /> 89 <BarLoader isLoading={loading} /> 90 <Command.List> 91 {initialized && ( 92 <> 93 {expoDocsItems.length > 0 && ( 94 <Command.Group heading="Expo documentation"> 95 {Object.values(expoDocsGroupedItems).map(values => 96 values 97 .sort((a, b) => a.url.localeCompare(a.baseUrl) - b.url.localeCompare(b.baseUrl)) 98 .slice(0, 6) 99 .map((item, index) => ( 100 <ExpoDocsItem 101 isNested={index !== 0} 102 item={item} 103 onSelect={dismiss} 104 key={`hit-expo-docs-${item.objectID}`} 105 /> 106 )) 107 )} 108 </Command.Group> 109 )} 110 {expoItems.length > 0 && ( 111 <Command.Group heading="Expo dashboard"> 112 {expoItems.map((item: ExpoItemType) => ( 113 <ExpoItem 114 item={item} 115 onSelect={dismiss} 116 key={`hit-expo-${item.url}`} 117 query={query} 118 /> 119 ))} 120 </Command.Group> 121 )} 122 {rnDocsItems.length > 0 && ( 123 <Command.Group heading="React Native documentation"> 124 {rnDocsItems.map(item => ( 125 <RNDocsItem item={item} onSelect={dismiss} key={`hit-rn-docs-${item.objectID}`} /> 126 ))} 127 </Command.Group> 128 )} 129 {directoryItems.length > 0 && ( 130 <Command.Group heading="React Native directory"> 131 {directoryItems.map(item => ( 132 <RNDirectoryItem 133 item={item} 134 onSelect={dismiss} 135 key={`hit-rn-dir-${item.npmPkg}`} 136 query={query} 137 /> 138 ))} 139 </Command.Group> 140 )} 141 {!loading && totalCount === 0 && ( 142 <Command.Empty> 143 <CALLOUT theme="secondary">No results found.</CALLOUT> 144 </Command.Empty> 145 )} 146 </> 147 )} 148 </Command.List> 149 <CommandFooter /> 150 </Command.Dialog> 151 ); 152}; 153