From 1a2e61eaf4a6abe9b48c13a85952a63e9a421002 Mon Sep 17 00:00:00 2001 From: Megan Leong Date: Sat, 3 Feb 2024 20:13:05 -0800 Subject: [PATCH 1/5] search bar with working functionality --- dfm-sideline-sidekick-app/App.tsx | 22 +++-- .../pages/GlobalSearch.tsx | 89 +++++++++++++++++++ 2 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 dfm-sideline-sidekick-app/pages/GlobalSearch.tsx diff --git a/dfm-sideline-sidekick-app/App.tsx b/dfm-sideline-sidekick-app/App.tsx index 3eefbad..ba166fc 100644 --- a/dfm-sideline-sidekick-app/App.tsx +++ b/dfm-sideline-sidekick-app/App.tsx @@ -1,21 +1,19 @@ import { StatusBar } from "expo-status-bar"; -import { StyleSheet, Text, View } from "react-native"; +import { StyleSheet, View } from "react-native"; + +import GlobalSearch from "./pages/GlobalSearch"; + +const styles = StyleSheet.create({ + container: { + justifyContent: "center", + }, +}); export default function App() { return ( - // eslint-disable-next-line @typescript-eslint/no-use-before-define - Open up App.js to start working on your app! + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#fff", - alignItems: "center", - justifyContent: "center", - }, -}); diff --git a/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx new file mode 100644 index 0000000..51d9b1a --- /dev/null +++ b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { FlatList, StyleSheet, Text, TextInput, View } from "react-native"; +import Icon from "react-native-vector-icons/FontAwesome"; + +type Document = { + id: string; + title: string; +}; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 17.5, + paddingTop: 50, + }, + title: { + color: "#182B49", + fontSize: 28, + fontFamily: "Roboto", + fontWeight: "700", + marginBottom: 20, + textAlign: "left", + paddingTop: 10, + }, + searchSection: { + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + borderWidth: 1, + borderColor: "rgba(0, 0, 0, 0.4)", + borderRadius: 10, + margin: 0, + }, + searchIcon: { + padding: 10, + }, + input: { + flex: 1, + paddingVertical: 10, + color: "#424242", + }, + itemTitle: { + padding: 10, + }, + }); + +const documents: Document[] = [ + { id: "1", title: "Emergency Action Plan" }, + { id: "2", title: "First Aid Procedures" }, + { id: "3", title: "Fire Safety Manual" }, +]; + +const SearchBarComponent = () => { + const [query, setQuery] = useState(""); + const [filteredDocuments, setFilteredDocuments] = useState([]); + + const handleSearch = (text: string) => { + setQuery(text); + if (!text.trim()) { + setFilteredDocuments([]); + } else { + const matchedDocuments = documents.filter((doc) => + doc.title.toLowerCase().includes(text.toLowerCase()), + ); + setFilteredDocuments(matchedDocuments); + } + }; + + return ( + + Global Search + + + + + item.id} + renderItem={({ item }) => {item.title}} + /> + + ); +}; + +export default SearchBarComponent; From a1066d952dfe14392a06b068c99261d030b19584 Mon Sep 17 00:00:00 2001 From: Megan Leong Date: Wed, 7 Feb 2024 21:59:36 -0800 Subject: [PATCH 2/5] styled the search results --- dfm-sideline-sidekick-app/App.tsx | 22 ++++--- .../pages/GlobalSearch.tsx | 59 ++++++----------- .../pages/GlobalSearchStyles.tsx | 64 +++++++++++++++++++ 3 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx diff --git a/dfm-sideline-sidekick-app/App.tsx b/dfm-sideline-sidekick-app/App.tsx index ba166fc..adb919d 100644 --- a/dfm-sideline-sidekick-app/App.tsx +++ b/dfm-sideline-sidekick-app/App.tsx @@ -1,19 +1,21 @@ import { StatusBar } from "expo-status-bar"; -import { StyleSheet, View } from "react-native"; - -import GlobalSearch from "./pages/GlobalSearch"; - -const styles = StyleSheet.create({ - container: { - justifyContent: "center", - }, -}); +import { StyleSheet, Text, View } from "react-native"; export default function App() { return ( + // eslint-disable-next-line @typescript-eslint/no-use-before-define - + Open up App.js to start working on your app! ); } + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#fff", + alignItems: "center", + justifyContent: "center", + }, +}); \ No newline at end of file diff --git a/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx index 51d9b1a..6086fe1 100644 --- a/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx +++ b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx @@ -1,52 +1,19 @@ import { useState } from "react"; -import { FlatList, StyleSheet, Text, TextInput, View } from "react-native"; +import { FlatList, Text, TextInput, View } from "react-native"; import Icon from "react-native-vector-icons/FontAwesome"; +import styles from "./GlobalSearchStyles"; + type Document = { id: string; title: string; + subtitle: string; }; -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 17.5, - paddingTop: 50, - }, - title: { - color: "#182B49", - fontSize: 28, - fontFamily: "Roboto", - fontWeight: "700", - marginBottom: 20, - textAlign: "left", - paddingTop: 10, - }, - searchSection: { - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - borderWidth: 1, - borderColor: "rgba(0, 0, 0, 0.4)", - borderRadius: 10, - margin: 0, - }, - searchIcon: { - padding: 10, - }, - input: { - flex: 1, - paddingVertical: 10, - color: "#424242", - }, - itemTitle: { - padding: 10, - }, - }); - const documents: Document[] = [ - { id: "1", title: "Emergency Action Plan" }, - { id: "2", title: "First Aid Procedures" }, - { id: "3", title: "Fire Safety Manual" }, + { 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 = () => { @@ -75,12 +42,22 @@ const SearchBarComponent = () => { placeholder="Search" value={query} onChangeText={handleSearch} + selectionColor="#909090" /> item.id} - renderItem={({ item }) => {item.title}} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + + {item.title} + {item.subtitle} + + + + )} /> ); diff --git a/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx b/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx new file mode 100644 index 0000000..ddc82e8 --- /dev/null +++ b/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx @@ -0,0 +1,64 @@ +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, + }, + 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, + }, + searchSection: { + flexDirection: "row", + 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, + }, + }); + export default styles; \ No newline at end of file From 9d200c0eef654a8fd90d8564bb5746ab95d2ef34 Mon Sep 17 00:00:00 2001 From: Megan Leong Date: Fri, 16 Feb 2024 02:35:00 -0800 Subject: [PATCH 3/5] added x and cancel buttons. allowed filtering to match for documents that partially match and tier the documents highest to lowest from most to least similar to the search query. highlight in blue parts of titles that match search query --- backend/tsconfig.json | 4 +- dfm-sideline-sidekick-app/App.tsx | 2 +- dfm-sideline-sidekick-app/HandleSearch.tsx | 54 +++++++ .../pages/GlobalSearch.tsx | 117 ++++++++++----- .../pages/GlobalSearchStyles.tsx | 138 ++++++++++-------- 5 files changed, 217 insertions(+), 98 deletions(-) create mode 100644 dfm-sideline-sidekick-app/HandleSearch.tsx diff --git a/backend/tsconfig.json b/backend/tsconfig.json index f5bc50e..fd409c0 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -104,6 +104,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */, - }, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } diff --git a/dfm-sideline-sidekick-app/App.tsx b/dfm-sideline-sidekick-app/App.tsx index adb919d..3eefbad 100644 --- a/dfm-sideline-sidekick-app/App.tsx +++ b/dfm-sideline-sidekick-app/App.tsx @@ -18,4 +18,4 @@ const styles = StyleSheet.create({ alignItems: "center", justifyContent: "center", }, -}); \ No newline at end of file +}); 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 index 6086fe1..ef49cfa 100644 --- a/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx +++ b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; -import { FlatList, Text, TextInput, View } from "react-native"; -import Icon from "react-native-vector-icons/FontAwesome"; +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"; @@ -11,9 +13,21 @@ type Document = { }; 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." }, + { + 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 = () => { @@ -22,43 +36,78 @@ const SearchBarComponent = () => { const handleSearch = (text: string) => { setQuery(text); - if (!text.trim()) { - setFilteredDocuments([]); - } else { - const matchedDocuments = documents.filter((doc) => - doc.title.toLowerCase().includes(text.toLowerCase()), + const matchedDocuments = searchDocuments(documents, text); + setFilteredDocuments(matchedDocuments); + }; + + const clearInput = () => { + setQuery(""); + setFilteredDocuments([]); + }; + + const highlightText = (text: string, input: string): React.ReactNode[] => { + // Escape special characters in the query for use in a regular expression + const escapedQuery = input.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); + // Build a regex to match the query as whole words + const queryRegex = new RegExp(`(\\b${escapedQuery})`, "gi"); + + // Split the title 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 title matches the query and is not just a whitespace + const isMatch = queryRegex.test(part) && part.trim() !== ""; + return ( + + {part} + ); - setFilteredDocuments(matchedDocuments); - } + }); }; return ( Global Search - - - + + + + + {query.length > 0 && ( + + + + )} + + + {query.length > 0 && ( + + Cancel + + )} + - item.id} - ItemSeparatorComponent={() => } - renderItem={({ item }) => ( - - - {item.title} - {item.subtitle} + {filteredDocuments.length > 0 && ( + item.id} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + + {highlightText(item.title, query)} + {item.subtitle} + + - - - )} - /> + )} + /> + )} ); }; diff --git a/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx b/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx index ddc82e8..fbc89d1 100644 --- a/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx +++ b/dfm-sideline-sidekick-app/pages/GlobalSearchStyles.tsx @@ -1,64 +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, - }, - 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, - }, - searchSection: { - flexDirection: "row", - 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, - }, - }); - export default styles; \ No newline at end of file + 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; From 8a97a754163289199d5ad7e8dec7f6e88c8411ad Mon Sep 17 00:00:00 2001 From: Megan Leong Date: Fri, 16 Feb 2024 03:23:45 -0800 Subject: [PATCH 4/5] highlight text function now highlights portions of partially matching titles correctly --- .../pages/GlobalSearch.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx index ef49cfa..63f9595 100644 --- a/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx +++ b/dfm-sideline-sidekick-app/pages/GlobalSearch.tsx @@ -46,19 +46,23 @@ const SearchBarComponent = () => { }; const highlightText = (text: string, input: string): React.ReactNode[] => { - // Escape special characters in the query for use in a regular expression - const escapedQuery = input.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); - // Build a regex to match the query as whole words - const queryRegex = new RegExp(`(\\b${escapedQuery})`, "gi"); + // Split the input into individual words and escape special characters for regex + const words = input.split(/\s+/).map((word) => word.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&")); - // Split the title by the regular expression to get an array of parts + // 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 title matches the query and is not just a whitespace + // 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} ); From ce0d73fca9571a5eedfa8c8251d5868eb68a65d4 Mon Sep 17 00:00:00 2001 From: Rohan Sachdeva Date: Fri, 16 Feb 2024 20:07:50 -0800 Subject: [PATCH 5/5] Attempt to fix backend linter issues --- backend/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index fd409c0..f5bc50e 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -104,6 +104,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + }, }