1import { createStackNavigator, HeaderBackButton, StackScreenProps } from '@react-navigation/stack';
2import Fuse from 'fuse.js';
3import React from 'react';
4import { Animated, Platform, StyleSheet, View } from 'react-native';
5import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
7import ExpoAPIIcon from '../components/ExpoAPIIcon';
8import SearchBar from '../components/SearchBar';
9import { Colors } from '../constants';
10import ComponentListScreen from './ComponentListScreen';
11import { ScreenItems as ApiScreenItems } from './ExpoApisScreen';
12import { ScreenItems as ComponentScreenItems } from './ExpoComponentsScreen';
13
14const fuse = new Fuse(ApiScreenItems.concat(ComponentScreenItems), { keys: ['name'] });
15
16const APPBAR_HEIGHT = Platform.OS === 'ios' ? 50 : 56;
17const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
18
19function Header({
20  children,
21  backButton,
22  tintColor,
23  navigation,
24}: {
25  children?: React.ReactNode;
26  backButton?: boolean;
27  tintColor?: string;
28  navigation: any;
29}) {
30  const { top } = useSafeAreaInsets();
31  // @todo: this is static and we don't know if it's visible or not on iOS.
32  // need to use a more reliable and cross-platform API when one exists, like
33  // LayoutContext. We also don't know if it's translucent or not on Android
34  // and depend on react-native-safe-area-view to tell us.
35  const STATUSBAR_HEIGHT = top || 8;
36
37  return (
38    <Animated.View
39      style={[
40        styles.container,
41        { paddingTop: STATUSBAR_HEIGHT, height: STATUSBAR_HEIGHT + APPBAR_HEIGHT },
42      ]}>
43      <View style={styles.appBar}>
44        <View style={[StyleSheet.absoluteFill, { flexDirection: 'row' }]}>
45          {backButton && (
46            <HeaderBackButton
47              onPress={() => navigation.goBack()}
48              pressColorAndroid={tintColor || '#fff'}
49              tintColor={tintColor}
50            />
51          )}
52          {children}
53        </View>
54      </View>
55    </Animated.View>
56  );
57}
58
59function SearchScreen({ route }: StackScreenProps<SearchStack, 'search'>) {
60  const query = route?.params?.q ?? '';
61
62  const apis = React.useMemo(() => fuse.search(query).map(({ item }) => item), [query]);
63
64  const renderItemRight = React.useCallback(
65    ({ name }: { name: string }) => (
66      <ExpoAPIIcon name={name} style={{ marginRight: 10, marginLeft: 6 }} />
67    ),
68    []
69  );
70
71  return <ComponentListScreen renderItemRight={renderItemRight} apis={apis} />;
72}
73
74type SearchStack = {
75  search: { q?: string };
76};
77
78const Stack = createStackNavigator<SearchStack>();
79
80export default () => (
81  <Stack.Navigator>
82    <Stack.Screen
83      name="search"
84      component={SearchScreen}
85      options={({ navigation, route }) => ({
86        header: () => (
87          <Header
88            navigation={navigation}
89            tintColor={Colors.tintColor}
90            backButton={Platform.OS === 'android'}>
91            <SearchBar
92              initialValue={route?.params?.q ?? ''}
93              onChangeQuery={(q) => navigation.setParams({ q })}
94              underlineColorAndroid="#fff"
95              tintColor={Colors.tintColor}
96            />
97          </Header>
98        ),
99      })}
100    />
101  </Stack.Navigator>
102);
103
104const styles = {
105  container: {
106    backgroundColor: '#fff',
107
108    ...Platform.select({
109      ios: {
110        borderBottomWidth: StyleSheet.hairlineWidth,
111        borderBottomColor: '#A7A7AA',
112      },
113      default: {
114        shadowColor: 'black',
115        shadowOpacity: 0.1,
116        shadowRadius: StyleSheet.hairlineWidth,
117        shadowOffset: {
118          width: 0,
119          height: StyleSheet.hairlineWidth,
120        },
121        elevation: 4,
122      },
123    }),
124  },
125  appBar: {
126    flex: 1,
127  },
128  header: {
129    flexDirection: 'row',
130  },
131  item: {
132    justifyContent: 'center',
133    alignItems: 'center',
134    backgroundColor: 'transparent',
135  },
136  title: {
137    bottom: 0,
138    left: TITLE_OFFSET,
139    right: TITLE_OFFSET,
140    top: 0,
141    position: 'absolute',
142    alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start',
143  },
144  left: {
145    left: 0,
146    bottom: 0,
147    top: 0,
148    position: 'absolute',
149  },
150  right: {
151    right: 0,
152    bottom: 0,
153    top: 0,
154    position: 'absolute',
155  },
156};
157