1import { SearchIcon, theme, XIcon } from '@expo/styleguide'; 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 <SearchIcon color={theme.icon.secondary} css={searchIconStyle} /> 75 <XIcon color={theme.icon.secondary} css={closeIconStyle} onClick={() => setOpen(false)} /> 76 <Command.Input value={query} onValueChange={setQuery} placeholder="search anything…" /> 77 <BarLoader isLoading={loading} /> 78 <Command.List> 79 {initialized && ( 80 <> 81 {expoDocsItems.length > 0 && ( 82 <Command.Group heading="Expo documentation"> 83 {expoDocsItems.map(item => ( 84 <ExpoDocsItem 85 item={item} 86 onSelect={dismiss} 87 key={`hit-expo-docs-${item.objectID}`} 88 /> 89 ))} 90 </Command.Group> 91 )} 92 {expoItems.length > 0 && ( 93 <Command.Group heading="Expo dashboard"> 94 {expoItems.map((item: ExpoItemType) => ( 95 <ExpoItem 96 item={item} 97 onSelect={dismiss} 98 key={`hit-expo-${item.url}`} 99 query={query} 100 /> 101 ))} 102 </Command.Group> 103 )} 104 {rnDocsItems.length > 0 && ( 105 <Command.Group heading="React Native documentation"> 106 {rnDocsItems.map(item => ( 107 <RNDocsItem item={item} onSelect={dismiss} key={`hit-rn-docs-${item.objectID}`} /> 108 ))} 109 </Command.Group> 110 )} 111 {directoryItems.length > 0 && ( 112 <Command.Group heading="React Native directory"> 113 {directoryItems.map(item => ( 114 <RNDirectoryItem 115 item={item} 116 onSelect={dismiss} 117 key={`hit-rn-dir-${item.npmPkg}`} 118 query={query} 119 /> 120 ))} 121 </Command.Group> 122 )} 123 {!loading && totalCount === 0 && ( 124 <Command.Empty> 125 <CALLOUT theme="secondary">No results found.</CALLOUT> 126 </Command.Empty> 127 )} 128 </> 129 )} 130 </Command.List> 131 <CommandFooter /> 132 </Command.Dialog> 133 ); 134}; 135