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