Skip to content

Commit

Permalink
Add search (close #9)
Browse files Browse the repository at this point in the history
  • Loading branch information
robertying committed Apr 12, 2019
1 parent 5babd94 commit 5f48e16
Show file tree
Hide file tree
Showing 8 changed files with 434 additions and 58 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@react-native-community/blur": "3.3.1",
"dayjs": "1.8.12",
"expo-secure-store": "4.0.0",
"fuse.js": "3.4.4",
"react": "16.8.6",
"react-native": "0.59.4",
"react-native-code-push": "5.6.0",
Expand Down
114 changes: 114 additions & 0 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useState } from "react";
import {
Animated,
TextInput,
TextInputProps,
View,
ViewProps
} from "react-native";
import { iOSUIKit } from "react-native-typography";
import Icon from "react-native-vector-icons/MaterialIcons";
import Colors from "../constants/Colors";
import TextButton, { ITextButtonProps } from "./TextButton";

export type ISearchBarProps = TextInputProps & {
readonly onCancel: ITextButtonProps["onPress"];
readonly onChangeText?: TextInputProps["onChangeText"];
readonly containerStyle?: ViewProps["style"];
readonly innerRef?: React.Ref<TextInput> | undefined;
};

const SearchBar: React.FunctionComponent<ISearchBarProps> = props => {
const { onCancel, containerStyle, innerRef, onChangeText } = props;

const [cancelButtonWidth] = useState(new Animated.Value(0.01));

const onFocus: TextInputProps["onFocus"] = e => {
Animated.timing(cancelButtonWidth, {
toValue: 34,
duration: 300
}).start();

const onFocus = props.onFocus;
if (onFocus) {
onFocus(e);
}
};

const onBlur: TextInputProps["onBlur"] = e => {
Animated.timing(cancelButtonWidth, {
toValue: 0.01,
duration: 300
}).start();

const onBlur = props.onBlur;
if (onBlur) {
onBlur(e);
}
};

return (
<Animated.View
style={[
{
backgroundColor: Colors.lightTint,
justifyContent: "center"
},
containerStyle
]}
>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextInput
ref={innerRef}
style={{
flex: 1,
height: 34,
margin: 8,
backgroundColor: "white",
borderColor: "white",
borderLeftWidth: 34,
borderRadius: 8,
fontSize: iOSUIKit.bodyObject.fontSize
}}
selectionColor={Colors.tint}
allowFontScaling={true}
clearButtonMode="while-editing"
enablesReturnKeyAutomatically={true}
placeholder="搜索课程、老师……"
placeholderTextColor="gray"
returnKeyType="search"
selectTextOnFocus={true}
onFocus={onFocus}
onBlur={onBlur}
onChangeText={onChangeText}
/>
<AnimatedButton
onPress={onCancel}
style={{
width: cancelButtonWidth,
marginRight: cancelButtonWidth.interpolate({
inputRange: [0, 34],
outputRange: [0, 8]
})
}}
>
取消
</AnimatedButton>
</View>
<Icon
name="search"
size={20}
style={{ position: "absolute", left: 20 }}
color="gray"
/>
</Animated.View>
);
};

const AnimatedButton = Animated.createAnimatedComponent(TextButton);

export default React.forwardRef(
(props: ISearchBarProps, ref: React.Ref<TextInput> | undefined) => (
<SearchBar innerRef={ref} {...props} />
)
);
37 changes: 19 additions & 18 deletions src/components/TextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,24 @@ export type ITextButtonProps = TouchableOpacityProps & {
readonly children: string;
};

const TextButton: React.FunctionComponent<ITextButtonProps> = props => {
const { textStyle, children } = props;

return (
<TouchableOpacity activeOpacity={Colors.activeOpacity} {...props}>
<Text
style={[
{ color: Colors.tint, fontSize: iOSUIKit.bodyObject.fontSize },
textStyle
]}
numberOfLines={1}
ellipsizeMode="clip"
>
{children}
</Text>
</TouchableOpacity>
);
};
class TextButton extends React.Component<ITextButtonProps> {
public render(): React.ReactElement {
const { textStyle, children } = this.props;
return (
<TouchableOpacity activeOpacity={Colors.activeOpacity} {...this.props}>
<Text
style={[
{ color: Colors.tint, fontSize: iOSUIKit.bodyObject.fontSize },
textStyle
]}
numberOfLines={1}
ellipsizeMode="clip"
>
{children}
</Text>
</TouchableOpacity>
);
}
}

export default TextButton;
85 changes: 74 additions & 11 deletions src/screens/AssignmentsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { useEffect } from "react";
import Fuse from "fuse.js";
import React, { useEffect, useRef, useState } from "react";
import { LayoutAnimation, SafeAreaView, TextInput } from "react-native";
import Icon from "react-native-vector-icons/MaterialIcons";
import { connect } from "react-redux";
import AssignmentsView from "../components/AssignmentsView";
import SearchBar from "../components/SearchBar";
import dayjs from "../helpers/dayjs";
import { initialRouteName } from "../navigation/MainTabNavigator";
import { getAllAssignmentsForCourses } from "../redux/actions/assignments";
Expand Down Expand Up @@ -86,22 +90,81 @@ const AssignmentsScreen: INavigationScreen<IAssignmentsScreenProps> = props => {
}
};

const isSearching = navigation.getParam("isSearching", false);
const searchBarRef = useRef<TextInput>();
const [searchResult, setSearchResult] = useState(assignments);

useEffect(() => {
if (isSearching) {
if (searchBarRef.current) {
searchBarRef.current.focus();
}
} else {
setSearchResult(assignments);
}
}, [isSearching]);

const onSearchChange = (text: string) => {
if (text) {
const fuse = new Fuse(assignments, fuseOptions);
setSearchResult(fuse.search(text));
}
};

return (
<AssignmentsView
isFetching={isFetching}
onRefresh={invalidateAll}
courses={courses}
assignments={assignments}
onAssignmentCardPress={onAssignmentCardPress}
/>
<SafeAreaView style={{ flex: 1 }}>
{isSearching && (
<SearchBar
ref={searchBarRef as any}
// tslint:disable-next-line: jsx-no-lambda
onCancel={() => {
LayoutAnimation.easeInEaseOut();
navigation.setParams({ isSearching: false });
}}
onChangeText={onSearchChange}
/>
)}
<AssignmentsView
isFetching={isFetching}
onRefresh={invalidateAll}
courses={courses}
assignments={searchResult}
onAssignmentCardPress={onAssignmentCardPress}
/>
</SafeAreaView>
);
};

// tslint:disable-next-line: no-object-mutation
AssignmentsScreen.navigationOptions = {
title: "作业"
const fuseOptions = {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ["attachmentName", "description", "title"]
};

// tslint:disable-next-line: no-object-mutation
AssignmentsScreen.navigationOptions = ({ navigation }) => ({
title: "作业",
headerRight: (
<Icon.Button
name="search"
// tslint:disable-next-line: jsx-no-lambda
onPress={() => {
LayoutAnimation.easeInEaseOut();
navigation.setParams({
isSearching: !navigation.getParam("isSearching", false)
});
}}
color="white"
backgroundColor="transparent"
underlayColor="transparent"
/>
)
});

function mapStateToProps(
state: IPersistAppState
): IAssignmentsScreenStateProps {
Expand Down
76 changes: 69 additions & 7 deletions src/screens/CoursesScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React, { useEffect } from "react";
import Fuse from "fuse.js";
import React, { useEffect, useRef, useState } from "react";
import {
FlatList,
LayoutAnimation,
ListRenderItem,
RefreshControl,
SafeAreaView
SafeAreaView,
TextInput
} from "react-native";
import { FlatList } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialIcons";
import { connect } from "react-redux";
import CoursePreviewView from "../components/CourseCard";
import Divider from "../components/Divider";
import SearchBar from "../components/SearchBar";
import Colors from "../constants/Colors";
import { initialRouteName } from "../navigation/MainTabNavigator";
import { getAllAssignmentsForCourses } from "../redux/actions/assignments";
Expand Down Expand Up @@ -111,6 +116,27 @@ const CoursesScreen: INavigationScreen<ICoursesScreenProps> = props => {
});
};

const isSearching = navigation.getParam("isSearching", false);
const searchBarRef = useRef<TextInput>();
const [searchResult, setSearchResult] = useState(courses);

useEffect(() => {
if (isSearching) {
if (searchBarRef.current) {
searchBarRef.current.focus();
}
} else {
setSearchResult(courses);
}
}, [isSearching]);

const onSearchChange = (text: string) => {
if (text) {
const fuse = new Fuse(courses, fuseOptions);
setSearchResult(fuse.search(text));
}
};

const renderListItem: ListRenderItem<ICourse> = ({ item }) => {
return (
<CoursePreviewView
Expand Down Expand Up @@ -145,9 +171,20 @@ const CoursesScreen: INavigationScreen<ICoursesScreenProps> = props => {

return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#f0f0f0" }}>
{isSearching && (
<SearchBar
ref={searchBarRef as any}
// tslint:disable-next-line: jsx-no-lambda
onCancel={() => {
LayoutAnimation.easeInEaseOut();
navigation.setParams({ isSearching: false });
}}
onChangeText={onSearchChange}
/>
)}
<FlatList
ItemSeparatorComponent={Divider}
data={courses}
data={searchResult}
renderItem={renderListItem}
keyExtractor={keyExtractor}
refreshControl={
Expand All @@ -162,11 +199,36 @@ const CoursesScreen: INavigationScreen<ICoursesScreenProps> = props => {
);
};

// tslint:disable-next-line: no-object-mutation
CoursesScreen.navigationOptions = {
title: "课程"
const fuseOptions = {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ["name", "teacherName"]
};

// tslint:disable-next-line: no-object-mutation
CoursesScreen.navigationOptions = ({ navigation }) => ({
title: "课程",
headerRight: (
<Icon.Button
name="search"
// tslint:disable-next-line: jsx-no-lambda
onPress={() => {
LayoutAnimation.easeInEaseOut();
navigation.setParams({
isSearching: !navigation.getParam("isSearching", false)
});
}}
color="white"
backgroundColor="transparent"
underlayColor="transparent"
/>
)
});

function mapStateToProps(state: IPersistAppState): ICoursesScreenStateProps {
return {
autoRefreshing: state.settings.autoRefreshing,
Expand Down
Loading

0 comments on commit 5f48e16

Please sign in to comment.