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