Files
imexmobile/components/global-search/global-search.jsx
2025-11-04 13:51:17 -08:00

124 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, KeyboardAvoidingView, Platform } from "react-native";
import { ActivityIndicator, Icon, 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();
const performSearch = useCallback(async (query) => {
// Defensive trimr
const q = (query || "").trim();
if (!q) return;
setLoading(true);
setError(null);
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, { id: "footer-spacer", height: 128 }]);
} else {
setError("No results available. Try again.");
}
} catch (error) {
console.error("Search error:", error, error.response);
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 (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Icon size={64} source="cloud-search" />
<Text variant="bodyMedium" style={{ margin: 12, textAlign: "center" }}>
{t("globalsearch.labels.entersearch")}
</Text>
</KeyboardAvoidingView>
);
}
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
{loading && <ActivityIndicator size="large" style={{ margin: 24 }} />}
{error && <ErrorDisplay errorMessage={error} />}
{!loading && (
<Text variant="titleSmall" style={{ margin: 12, alignSelf: "center" }}>
{
Math.max(0, results.length - 1) //Need to subtract for the spacer.
}{" "}
results found
</Text>
)}
<FlatList
style={{ flex: 1 }}
data={results}
keyExtractor={(item) => item.id?.toString()}
renderItem={(object) => <JobListItem item={object.item} />}
/>
</KeyboardAvoidingView>
);
}