diff --git a/dfm-sideline-sidekick-app/HandleSearch.tsx b/dfm-sideline-sidekick-app/HandleSearch.tsx new file mode 100644 index 0000000..ae0fd55 --- /dev/null +++ b/dfm-sideline-sidekick-app/HandleSearch.tsx @@ -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 })); +}; diff --git a/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx new file mode 100644 index 0000000..63f9595 --- /dev/null +++ b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx @@ -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([]); + + const handleSearch = (text: string) => { + 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 ( + + {part} + + ); + }); + }; + + return ( + + Global Search + + + + + {query.length > 0 && ( + + + + )} + + + {query.length > 0 && ( + + Cancel + + )} + + + {filteredDocuments.length > 0 && ( + item.id} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + + {highlightText(item.title, query)} + {item.subtitle} + + + + )} + /> + )} + + ); +}; + +export default SearchBarComponent; diff --git a/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx b/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx new file mode 100644 index 0000000..fbc89d1 --- /dev/null +++ b/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx @@ -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;