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