Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global Search Page #13

Merged
merged 5 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions dfm-sideline-sidekick-app/HandleSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
type Document = {
id: string;
title: string;
subtitle: string;
};

export const searchDocuments = (documents: Document[], searchText: string): Document[] => {
if (!searchText.trim()) {
return [];
}

// Lowercase the search text for case-insensitive comparisons
const lowerSearchText = searchText.toLowerCase();

// Score each document based on how well it matches the search text
const scoredDocs = documents.map((doc) => {
const lowerTitle = doc.title.toLowerCase();
let score = 0;

// Exact match scores highest
if (lowerTitle === lowerSearchText) {
score = 100;
}
// Starting match scores higher
else if (lowerTitle.startsWith(lowerSearchText)) {
score = 75;
}
// Contains the search text scores lower
else if (lowerTitle.includes(lowerSearchText)) {
score = 50;
}
// Check if each word in the search text is contained in the title
else {
const searchTextWords = lowerSearchText.split(/\s+/);
const titleWords = lowerTitle.split(/\s+/);
searchTextWords.forEach((searchWord) => {
if (titleWords.includes(searchWord)) {
score += 5;
}
});
}

return { ...doc, score };
});

// Filter out documents that don't match at all
const filteredDocs = scoredDocs.filter((doc) => doc.score > 0);

// Sort by score in descending order
const sortedDocs = filteredDocs.sort((a, b) => b.score - a.score);

// Return the documents sorted by their score
return sortedDocs.map((doc) => ({ id: doc.id, title: doc.title, subtitle: doc.subtitle }));
};
119 changes: 119 additions & 0 deletions dfm-sideline-sidekick-app/pages/GlobalSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useState } from "react";
import { FlatList, Text, TextInput, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/Feather";

import { searchDocuments } from "../HandleSearch";

import styles from "./GlobalSearchStyles";

type Document = {
id: string;
title: string;
subtitle: string;
};

const documents: Document[] = [
{
id: "1",
title: "Cervical Spine Injury",
subtitle: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do.",
},
{
id: "2",
title: "Cervical Strain",
subtitle: "Lorem ipsum dolor sit amet, consectetur adipiscing.",
},
{
id: "3",
title: "Stroke",
subtitle: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.",
},
];

const SearchBarComponent = () => {
const [query, setQuery] = useState("");
const [filteredDocuments, setFilteredDocuments] = useState<Document[]>([]);

const handleSearch = (text: string) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the future, maybe extract the handleSearch functionality to a separate file, as this will gain complexity when having to search through device storage

setQuery(text);
const matchedDocuments = searchDocuments(documents, text);
setFilteredDocuments(matchedDocuments);
};

const clearInput = () => {
setQuery("");
setFilteredDocuments([]);
};

const highlightText = (text: string, input: string): React.ReactNode[] => {
// Split the input into individual words and escape special characters for regex
const words = input.split(/\s+/).map((word) => word.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"));

// Create a regex pattern that matches any of the words
const pattern = words.join("|"); // Join the words with the regex 'or' operator
const queryRegex = new RegExp(`(${pattern})`, "gi");

// Split the text by the regular expression to get an array of parts
const parts = text.split(queryRegex);

return parts.map((part, index) => {
// Check if the part of the text matches any of the words in the query
const isMatch = queryRegex.test(part) && part.trim() !== "";
// Reset lastIndex because of the global regex test side effect
queryRegex.lastIndex = 0;
return (
<Text key={index.toString()} style={isMatch ? styles.highlightedText : undefined}>
{part}
</Text>
);
});
};

return (
<View style={styles.container}>
<Text style={styles.title}>Global Search</Text>
<View style={styles.searchContainer}>
<View style={styles.searchSection}>
<Icon name="search" size={13} color="gray" style={styles.searchIcon} />
<TextInput
style={styles.input}
placeholder="Search"
value={query}
onChangeText={handleSearch}
selectionColor="#909090"
/>
{query.length > 0 && (
<TouchableOpacity onPress={clearInput} style={{ padding: 10 }}>
<Icon name="x" size={15} color="gray" />
</TouchableOpacity>
)}
</View>
<View>
{query.length > 0 && (
<TouchableOpacity onPress={clearInput} style={styles.cancelButton}>
<Text>Cancel</Text>
</TouchableOpacity>
)}
</View>
</View>
{filteredDocuments.length > 0 && (
<FlatList
data={filteredDocuments}
keyExtractor={(item) => item.id}
ItemSeparatorComponent={() => <View style={styles.divider} />}
renderItem={({ item }) => (
<View style={styles.listItemContainer}>
<View style={styles.listItemTextContainer}>
<Text style={styles.listItemTitle}>{highlightText(item.title, query)}</Text>
<Text style={styles.listItemSubtitle}>{item.subtitle}</Text>
</View>
<Icon name="chevron-right" size={20} color="#909090" />
</View>
)}
/>
)}
</View>
);
};

export default SearchBarComponent;
80 changes: 80 additions & 0 deletions dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { StyleSheet } from "react-native";

const styles = StyleSheet.create({
container: {
paddingHorizontal: 17.5,
paddingTop: 50,
},
listItemContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingVertical: 10,
paddingHorizontal: 15,
},
cancelButton: {
paddingLeft: 10,
marginBottom: 8,
justifyContent: "center",
alignItems: "center",
overflow: "visible",
},
listItemTextContainer: {
flex: 1,
},
listItemTitle: {
fontSize: 18,
fontWeight: "500",
paddingBottom: 10,
},
listItemSubtitle: {
fontSize: 13,
color: "grey",
},
divider: {
height: 1,
backgroundColor: "lightgrey",
marginHorizontal: 15,
marginVertical: 10,
},
title: {
color: "#182B49",
fontSize: 28,
fontFamily: "Roboto",
fontWeight: "700",
marginBottom: 20,
textAlign: "left",
paddingTop: 10,
},
searchContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
searchSection: {
flexDirection: "row",
flex: 1,
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
borderColor: "rgba(0, 0, 0, 0.4)",
borderRadius: 10,
margin: 0,
marginBottom: 10,
},
searchIcon: {
padding: 10,
},
input: {
flex: 1,
paddingVertical: 10,
color: "#424242",
},
itemTitle: {
padding: 10,
},
highlightedText: {
color: "#00629B",
},
});
export default styles;
Loading