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