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 connected to downloaded data #22

Merged
merged 19 commits into from
Mar 12, 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
82 changes: 42 additions & 40 deletions dfm-sideline-sidekick-app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { StackNavigationProp } from "@react-navigation/stack";
import { StatusBar } from "expo-status-bar";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React, { useEffect } from "react";
import { Platform, StyleSheet } from "react-native";
import React from "react";
import { StyleSheet } from "react-native";

import AppInitializer from "./AppInitializer";
import { DataProvider } from "./DataContext";
import { BottomNavBar, NavItem } from "./components/bar";
import { checkConnection } from "./download/connection/checkConnection";
import { downloadJSON } from "./download/downloadFromAPI";
import BookmarkPage from "./pages/BookmarkPage";
import ConditionsSection from "./pages/ConditionsSection";
import SearchPage from "./pages/SearchPage";
import TabPage from "./pages/TabPage";
import GeneralPrinciples from "./pages/generalPrinciples";

type DocumentBase = {
_id: string;
title: string;
subtitle?: string;
overview?: object;
treatment?: object;
content?: object;
};

type RootStackParamList = {
Bookmark: undefined;
Search: undefined;
Tab: undefined;
MedicalConditions: { emergency: DocumentBase };
GeneralPrinciples: { contentProp: DocumentBase };
};

type StackNavigation = StackNavigationProp<RootStackParamList>;
Expand All @@ -28,20 +41,23 @@ const BottomNavBarComponent = () => {
const navigationItems: NavItem[] = [
{
id: 1,
routeName: "Bookmark",
icon: "bookmark",
onClick: () => {
navigation.navigate("Bookmark");
},
},
{
id: 2,
routeName: "Search",
icon: "search",
onClick: () => {
navigation.navigate("Search");
},
},
{
id: 3,
routeName: "Principles",
icon: "principles",
onClick: () => {
navigation.navigate("Tab");
Expand All @@ -53,43 +69,29 @@ const BottomNavBarComponent = () => {
};

export default function App() {
const deviceType = Platform.OS;

// makes it so that it only checks the version once per app launch
let attempted = false;

// true when there's connection
let connected = false;

// checks on app open, connect change
useEffect(() => {
// stores if connected
console.log("ATTEMPTED BEFORE:", attempted);

async function matchConditions() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
connected = await checkConnection();
// if also connected, attempt to redownload
if (connected && !attempted) {
await downloadJSON("data.json", deviceType);

attempted = true; // latches
}
}

void matchConditions();
}, [connected]);

return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Search">
<Stack.Screen name="Bookmark" component={BookmarkPage} options={{ headerShown: false }} />
<Stack.Screen name="Search" component={SearchPage} options={{ headerShown: false }} />
<Stack.Screen name="Tab" component={TabPage} options={{ headerShown: false }} />
</Stack.Navigator>
<BottomNavBarComponent />
<StatusBar style="auto" />
</NavigationContainer>
<DataProvider>
<AppInitializer />
<NavigationContainer>
<Stack.Navigator initialRouteName="Search">
<Stack.Screen name="Bookmark" component={BookmarkPage} options={{ headerShown: false }} />
<Stack.Screen name="Search" component={SearchPage} options={{ headerShown: false }} />
<Stack.Screen name="Tab" component={TabPage} options={{ headerShown: false }} />
<Stack.Screen
name="MedicalConditions"
component={ConditionsSection}
options={{ headerShown: false }}
/>
<Stack.Screen
name="GeneralPrinciples"
component={GeneralPrinciples}
options={{ headerShown: false }}
/>
</Stack.Navigator>
<BottomNavBarComponent />
<StatusBar style="auto" />
</NavigationContainer>
</DataProvider>
);
}

Expand Down
43 changes: 43 additions & 0 deletions dfm-sideline-sidekick-app/AppInitializer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect } from "react";
import { Platform } from "react-native";

import { useData } from "./DataContext";
import { checkConnection } from "./download/connection/checkConnection";
import { downloadJSON } from "./download/downloadFromAPI";

function AppInitializer() {
const { updateJsonData } = useData();

const deviceType = Platform.OS;

// makes it so that it only checks the version once per app launch
let attempted = false;

// true when there's connection
let connected = false;

// checks on app open, connect change
useEffect(() => {
// stores if connected
console.log("ATTEMPTED BEFORE:", attempted);

async function matchConditions() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
connected = await checkConnection();
// if also connected, attempt to redownload
if (connected && !attempted) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const jsonData = await downloadJSON("data.json", deviceType);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
updateJsonData(jsonData);

attempted = true; // latches
}
}

void matchConditions();
}, [connected]);
return null;
}

export default AppInitializer;
45 changes: 45 additions & 0 deletions dfm-sideline-sidekick-app/DataContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { ReactNode, createContext, useContext, useState } from "react";

type MedicalEmergency = {
title: string;
overview: object;
treatment: object;
content: object;
};

type GeneralPrinciple = {
title: string;
overview: object;
content: object;
};

type JsonDataStructure = {
emergencies?: MedicalEmergency[]; // Optional array of MedicalEmergency
generalPrinciples?: GeneralPrinciple[]; // Optional array of GeneralPrinciple
};

type DataContextType = {
jsonData: JsonDataStructure | null;
updateJsonData: (data: JsonDataStructure) => void;
};
const DataContext = createContext<DataContextType | undefined>(undefined);

export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [jsonData, setJsonData] = useState<JsonDataStructure | null>(null);

const updateJsonData = (data: JsonDataStructure) => {
setJsonData(data);
};

return (
<DataContext.Provider value={{ jsonData, updateJsonData }}>{children}</DataContext.Provider>
);
};

export const useData = () => {
const context = useContext(DataContext);
if (context === undefined) {
throw new Error("useData must be used within a DataProvider");
}
return context;
};
26 changes: 8 additions & 18 deletions dfm-sideline-sidekick-app/HandleSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
type Document = {
id: string;
_id?: string;
title: string;
subtitle: string;
subtitle?: string;
overview?: object;
treatment?: object;
};

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)) {
} else if (lowerTitle.startsWith(lowerSearchText)) {
score = 75;
}
// Contains the search text scores lower
else if (lowerTitle.includes(lowerSearchText)) {
} else if (lowerTitle.includes(lowerSearchText)) {
score = 50;
}
// Check if each word in the search text is contained in the title
else {
} else {
const searchTextWords = lowerSearchText.split(/\s+/);
const titleWords = lowerTitle.split(/\s+/);
searchTextWords.forEach((searchWord) => {
Expand All @@ -43,12 +36,9 @@ export const searchDocuments = (documents: Document[], searchText: string): Docu
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 }));
return sortedDocs.map(({ score: _score, ...doc }) => doc);
};
57 changes: 57 additions & 0 deletions dfm-sideline-sidekick-app/SearchBarComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import { Text, TextInput, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/Feather";

import styles from "./SearchBarStyles";

type SearchBarProps = {
query: string;
setQuery: (query: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onClear: () => void;
onCancel: () => void;
isFocused: boolean;
inputRef: React.RefObject<TextInput>;
};

const SearchBar: React.FC<SearchBarProps> = ({
query,
setQuery,
onFocus,
onBlur,
onClear,
onCancel,
isFocused,
inputRef,
}) => {
return (
<View style={styles.searchContainer}>
<View style={styles.searchSection}>
<Icon name="search" size={20} color="gray" style={styles.searchIcon} />
<TextInput
ref={inputRef}
style={styles.input}
placeholder="Search"
value={query}
onChangeText={setQuery}
onFocus={onFocus}
onBlur={onBlur}
selectionColor="#909090"
/>
{query.length > 0 && (
<TouchableOpacity onPress={onClear} style={{ padding: 10 }}>
<Icon name="x" size={20} color="gray" />
</TouchableOpacity>
)}
</View>
{isFocused && (
<TouchableOpacity onPress={onCancel} style={styles.cancelButton}>
<Text>Cancel</Text>
</TouchableOpacity>
)}
</View>
);
};

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

const styles = StyleSheet.create({
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,
},
input: {
flex: 1,
paddingVertical: 10,
color: "#424242",
},
searchIcon: {
padding: 10,
},
cancelButton: {
paddingLeft: 10,
marginBottom: 8,
justifyContent: "center",
alignItems: "center",
overflow: "visible",
},
});

export default styles;
1 change: 0 additions & 1 deletion dfm-sideline-sidekick-app/components/ArrayPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type ArrayProps = {

const temp = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ArrayPage: React.FC<ArrayProps> = ({ arrayProp, title }) => {
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();

Expand Down
2 changes: 1 addition & 1 deletion dfm-sideline-sidekick-app/components/StringRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { Text, View } from "react-native";

import styles from "../ConditionSectionStyles";
import styles from "../pages/ConditionSectionStyles";

type StringValue = string | string[] | { [key: string]: StringValue } | undefined;

Expand Down
Loading
Loading