111 lines
3.5 KiB
JavaScript
111 lines
3.5 KiB
JavaScript
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, 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();
|
||
// 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" }}>
|
||
<Icon size={64} source="cloud-search" />
|
||
<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>
|
||
);
|
||
}
|