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 }) => <ExpoAPIIcon name={name} style={{ marginRight: 10, marginLeft: 6 }} />,
66    []
67  );
68
69  return <ComponentListScreen renderItemRight={renderItemRight} apis={apis} />;
70}
71
72type SearchStack = {
73  search: { q?: string };
74};
75
76const Stack = createStackNavigator<SearchStack>();
77
78export default () => (
79  <Stack.Navigator>
80    <Stack.Screen
81      name="search"
82      component={SearchScreen}
83      options={({ navigation, route }) => ({
84        header: () => (
85          <Header
86            navigation={navigation}
87            tintColor={Colors.tintColor}
88            backButton={Platform.OS === 'android'}>
89            <SearchBar
90              initialValue={route?.params?.q ?? ''}
91              onChangeQuery={q => navigation.setParams({ q })}
92              underlineColorAndroid="#fff"
93              tintColor={Colors.tintColor}
94            />
95          </Header>
96        ),
97      })}
98    />
99  </Stack.Navigator>
100);
101
102const styles = {
103  container: {
104    backgroundColor: '#fff',
105
106    ...Platform.select({
107      ios: {
108        borderBottomWidth: StyleSheet.hairlineWidth,
109        borderBottomColor: '#A7A7AA',
110      },
111      default: {
112        shadowColor: 'black',
113        shadowOpacity: 0.1,
114        shadowRadius: StyleSheet.hairlineWidth,
115        shadowOffset: {
116          width: 0,
117          height: StyleSheet.hairlineWidth,
118        },
119        elevation: 4,
120      },
121    }),
122  },
123  appBar: {
124    flex: 1,
125  },
126  header: {
127    flexDirection: 'row',
128  },
129  item: {
130    justifyContent: 'center',
131    alignItems: 'center',
132    backgroundColor: 'transparent',
133  },
134  title: {
135    bottom: 0,
136    left: TITLE_OFFSET,
137    right: TITLE_OFFSET,
138    top: 0,
139    position: 'absolute',
140    alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start',
141  },
142  left: {
143    left: 0,
144    bottom: 0,
145    top: 0,
146    position: 'absolute',
147  },
148  right: {
149    right: 0,
150    bottom: 0,
151    top: 0,
152    position: 'absolute',
153  },
154};
155