Improve search layouts.

This commit is contained in:
Patrick Fic
2025-10-22 15:07:57 -07:00
parent 49d40aa038
commit c62d2ab05f
9 changed files with 546 additions and 328 deletions

View File

@@ -50,9 +50,7 @@ function AuthenticatedLayout() {
{Platform.select({ {Platform.select({
//ios: <Icon sf="checklist" drawable="custom_android_drawable" />, //ios: <Icon sf="checklist" drawable="custom_android_drawable" />,
android: ( android: (
<Icon <Icon src={<VectorIcon family={MaterialIcons} name="search" />} />
src={<VectorIcon family={MaterialIcons} name="search" />}
/>
), ),
})} })}
<Label>Search</Label> <Label>Search</Label>

View File

@@ -11,6 +11,8 @@ export default function SearchLayout() {
headerSearchBarOptions: { headerSearchBarOptions: {
placement: "automatic", placement: "automatic",
placeholder: "Search", placeholder: "Search",
autoFocus: true,
shouldShowHintSearchIcon: true,
onChangeText: (event) => { onChangeText: (event) => {
router.setParams({ router.setParams({
globalSearch: event?.nativeEvent?.text, globalSearch: event?.nativeEvent?.text,

View File

@@ -1,12 +1,4 @@
import { useLocalSearchParams } from "expo-router"; import GlobalSearch from "../../components/global-search/global-search";
import { ScrollView } from "react-native";
import { Text } from "react-native-paper";
export default function SearchIndex() { export default function SearchIndex() {
const { globalSearch } = useLocalSearchParams(); return <GlobalSearch />;
return (
<ScrollView>
<Text>Some search results here for: {globalSearch}</Text>
</ScrollView>
);
} }

View File

@@ -131,6 +131,27 @@
<folder_node> <folder_node>
<name>labels</name> <name>labels</name>
<children> <children>
<concept_node>
<name>error</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>na</name> <name>na</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -152,6 +173,58 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>uploadprogress</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>globalsearch</name>
<children>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>entersearch</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
</children> </children>
@@ -4932,6 +5005,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>wronginfo</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>wrongpassword</name> <name>wrongpassword</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -0,0 +1,109 @@
import axios from "axios";
import { useLocalSearchParams } from "expo-router";
import debounce from "lodash/debounce";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, View } from "react-native";
import { ActivityIndicator, Text } from "react-native-paper";
import env from "../../env";
import ErrorDisplay from "../error/error-display";
import JobListItem from "../jobs-list/job-list-item";
// Debounce delay (ms) adjust as needed
const GLOBAL_SEARCH_DEBOUNCE_MS = 400;
/**
* Hook returning a debounced search trigger.
* It recreates the debounced function only when the underlying callback changes.
* Placeholder: Replace the body of `performSearch` with real API / GraphQL logic.
*/
function useDebouncedGlobalSearch(onSearch, delay = GLOBAL_SEARCH_DEBOUNCE_MS) {
const debouncedRef = useRef(() => {});
useEffect(() => {
// Create debounced wrapper
const debounced = debounce((query) => {
onSearch(query);
}, delay);
debouncedRef.current = debounced;
return () => debounced.cancel();
}, [onSearch, delay]);
return useCallback((query) => {
debouncedRef.current && debouncedRef.current(query);
}, []);
}
export default function GlobalSearch() {
const { globalSearch } = useLocalSearchParams();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [results, setResults] = useState([]);
const { t } = useTranslation();
// Placeholder: Replace with actual API call (e.g., Apollo client query, REST fetch, Redux saga dispatch)
const performSearch = useCallback(async (query) => {
// Defensive trimr
const q = (query || "").trim();
if (!q) return;
setLoading(true);
setError(null);
// TODO: Integrate real search endpoint
console.log(`[GlobalSearch] (debounced placeholder) searching for: "${q}"`);
try {
const searchData = await axios.post(`${env.API_URL}/search`, {
search: q,
});
if (searchData.data) {
const jobResults = searchData.data?.hits?.hits
?.filter((hit) => hit._index === "jobs")
.map((hit) => hit._source);
setResults(jobResults);
} else {
setError("No results available. Try again.");
}
} catch (error) {
console.error("Search error:", error);
setError(error.message);
}
setLoading(false);
}, []);
const debouncedSearch = useDebouncedGlobalSearch(performSearch);
// Trigger debounced search when the route param changes
useEffect(() => {
if (typeof globalSearch === "string" && globalSearch.length > 0) {
debouncedSearch(globalSearch);
}
}, [globalSearch, debouncedSearch]);
if (globalSearch === undefined || globalSearch.trim() === "") {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text variant="bodyMedium" style={{ margin: 12, textAlign: "center" }}>
{t("globalsearch.labels.entersearch")}
</Text>
</View>
);
}
return (
<View style={{ flex: 1 }}>
{loading && <ActivityIndicator size="large" />}
{error && <ErrorDisplay errorMessage={error} />}
<Text variant="titleSmall" style={{ margin: 12, alignSelf: "center" }}>
{results.length} results found
</Text>
<FlatList
style={{ flex: 1 }}
data={results}
keyExtractor={(item) => item.id?.toString()}
renderItem={(object) => <JobListItem item={object.item} />}
/>
</View>
);
}

View File

@@ -3,7 +3,7 @@ import { useRouter } from "expo-router";
import React, { memo, useCallback } from "react"; import React, { memo, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Pressable, StyleSheet, View } from "react-native"; import { Pressable, StyleSheet, View } from "react-native";
import { IconButton, Text, useTheme } from "react-native-paper"; import { Chip, IconButton, Text, useTheme } from "react-native-paper";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.analytics"; import { logImEXEvent } from "../../firebase/firebase.analytics";
@@ -86,6 +86,7 @@ function JobListItemComponent({ openImagePicker, item }) {
{vehicle} {vehicle}
</Text> </Text>
)} )}
<Chip style>{item.status}</Chip>
</View> </View>
</View> </View>
<IconButton <IconButton
@@ -158,6 +159,7 @@ const styles = StyleSheet.create({
}, },
body: { body: {
marginTop: 2, marginTop: 2,
flexDirection: "row", gap: 8, alignItems: "center",
}, },
ownerText: { ownerText: {
fontWeight: "600", fontWeight: "600",

View File

@@ -14,11 +14,16 @@
"signout": "Sign Out" "signout": "Sign Out"
}, },
"labels": { "labels": {
"na": "N/A",
"error": "Error", "error": "Error",
"na": "N/A",
"uploadprogress": "Upload Progress" "uploadprogress": "Upload Progress"
} }
}, },
"globalsearch": {
"labels": {
"entersearch": "Recent items"
}
},
"jobdetail": { "jobdetail": {
"labels": { "labels": {
"claiminformation": "Claim Information", "claiminformation": "Claim Information",

View File

@@ -14,7 +14,14 @@
"signout": "" "signout": ""
}, },
"labels": { "labels": {
"na": "" "error": "",
"na": "",
"uploadprogress": ""
}
},
"globalsearch": {
"labels": {
"entersearch": ""
} }
}, },
"jobdetail": { "jobdetail": {
@@ -299,6 +306,7 @@
"errors": { "errors": {
"emailformat": "", "emailformat": "",
"usernotfound": "", "usernotfound": "",
"wronginfo": "",
"wrongpassword": "" "wrongpassword": ""
}, },
"fields": { "fields": {

View File

@@ -14,7 +14,14 @@
"signout": "" "signout": ""
}, },
"labels": { "labels": {
"na": "" "error": "",
"na": "",
"uploadprogress": ""
}
},
"globalsearch": {
"labels": {
"entersearch": ""
} }
}, },
"jobdetail": { "jobdetail": {
@@ -299,6 +306,7 @@
"errors": { "errors": {
"emailformat": "", "emailformat": "",
"usernotfound": "", "usernotfound": "",
"wronginfo": "",
"wrongpassword": "" "wrongpassword": ""
}, },
"fields": { "fields": {