Improve search layouts.
This commit is contained in:
@@ -50,9 +50,7 @@ function AuthenticatedLayout() {
|
||||
{Platform.select({
|
||||
//ios: <Icon sf="checklist" drawable="custom_android_drawable" />,
|
||||
android: (
|
||||
<Icon
|
||||
src={<VectorIcon family={MaterialIcons} name="search" />}
|
||||
/>
|
||||
<Icon src={<VectorIcon family={MaterialIcons} name="search" />} />
|
||||
),
|
||||
})}
|
||||
<Label>Search</Label>
|
||||
|
||||
@@ -11,6 +11,8 @@ export default function SearchLayout() {
|
||||
headerSearchBarOptions: {
|
||||
placement: "automatic",
|
||||
placeholder: "Search",
|
||||
autoFocus: true,
|
||||
shouldShowHintSearchIcon: true,
|
||||
onChangeText: (event) => {
|
||||
router.setParams({
|
||||
globalSearch: event?.nativeEvent?.text,
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { ScrollView } from "react-native";
|
||||
import { Text } from "react-native-paper";
|
||||
|
||||
import GlobalSearch from "../../components/global-search/global-search";
|
||||
export default function SearchIndex() {
|
||||
const { globalSearch } = useLocalSearchParams();
|
||||
return (
|
||||
<ScrollView>
|
||||
<Text>Some search results here for: {globalSearch}</Text>
|
||||
</ScrollView>
|
||||
);
|
||||
return <GlobalSearch />;
|
||||
}
|
||||
|
||||
@@ -131,6 +131,27 @@
|
||||
<folder_node>
|
||||
<name>labels</name>
|
||||
<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>
|
||||
<name>na</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -152,6 +173,58 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</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>
|
||||
</folder_node>
|
||||
</children>
|
||||
@@ -4932,6 +5005,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</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>
|
||||
<name>wrongpassword</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
109
components/global-search/global-search.jsx
Normal file
109
components/global-search/global-search.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "expo-router";
|
||||
import React, { memo, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||
@@ -86,6 +86,7 @@ function JobListItemComponent({ openImagePicker, item }) {
|
||||
{vehicle}
|
||||
</Text>
|
||||
)}
|
||||
<Chip style>{item.status}</Chip>
|
||||
</View>
|
||||
</View>
|
||||
<IconButton
|
||||
@@ -158,6 +159,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
body: {
|
||||
marginTop: 2,
|
||||
flexDirection: "row", gap: 8, alignItems: "center",
|
||||
},
|
||||
ownerText: {
|
||||
fontWeight: "600",
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
"signout": "Sign Out"
|
||||
},
|
||||
"labels": {
|
||||
"na": "N/A",
|
||||
"error": "Error",
|
||||
"na": "N/A",
|
||||
"uploadprogress": "Upload Progress"
|
||||
}
|
||||
},
|
||||
"globalsearch": {
|
||||
"labels": {
|
||||
"entersearch": "Recent items"
|
||||
}
|
||||
},
|
||||
"jobdetail": {
|
||||
"labels": {
|
||||
"claiminformation": "Claim Information",
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
"signout": ""
|
||||
},
|
||||
"labels": {
|
||||
"na": ""
|
||||
"error": "",
|
||||
"na": "",
|
||||
"uploadprogress": ""
|
||||
}
|
||||
},
|
||||
"globalsearch": {
|
||||
"labels": {
|
||||
"entersearch": ""
|
||||
}
|
||||
},
|
||||
"jobdetail": {
|
||||
@@ -299,6 +306,7 @@
|
||||
"errors": {
|
||||
"emailformat": "",
|
||||
"usernotfound": "",
|
||||
"wronginfo": "",
|
||||
"wrongpassword": ""
|
||||
},
|
||||
"fields": {
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
"signout": ""
|
||||
},
|
||||
"labels": {
|
||||
"na": ""
|
||||
"error": "",
|
||||
"na": "",
|
||||
"uploadprogress": ""
|
||||
}
|
||||
},
|
||||
"globalsearch": {
|
||||
"labels": {
|
||||
"entersearch": ""
|
||||
}
|
||||
},
|
||||
"jobdetail": {
|
||||
@@ -299,6 +306,7 @@
|
||||
"errors": {
|
||||
"emailformat": "",
|
||||
"usernotfound": "",
|
||||
"wronginfo": "",
|
||||
"wrongpassword": ""
|
||||
},
|
||||
"fields": {
|
||||
|
||||
Reference in New Issue
Block a user