141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
import { View, Text, FlatList } from "react-native";
|
|
import { useLocalSearchParams, router } from "expo-router";
|
|
import { ActivityIndicator } from "react-native-paper";
|
|
import { useAppSelector } from "@/store/hooks";
|
|
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
|
|
|
import { SearchResult, useGlobalSearchQuery } from "@/store/api/searchApi";
|
|
import { SearchBar } from "@/components/ui/SearchBar";
|
|
import { SizeEnum } from "@/constants/sizeEnum";
|
|
import { ResultRow } from "@/components/features/search/ResultRow";
|
|
import { normalizeEnumLike } from "@/utils/formattingUtils";
|
|
import { useDebounce } from "@/hooks/useDebounce";
|
|
import { Button } from "@/components/ui/Button";
|
|
|
|
const SearchPage = () => {
|
|
const params = useLocalSearchParams<{ q?: string }>();
|
|
const [q, setQ] = useState(params.q ?? "");
|
|
const isMobile = useAppSelector(selectIsMobile);
|
|
|
|
useEffect(() => {
|
|
setQ(params.q ?? "");
|
|
}, [params.q]);
|
|
|
|
const debouncedQ = useDebounce(q, 300);
|
|
|
|
const [limit] = useState(100);
|
|
const { data, isFetching, isLoading, isError } = useGlobalSearchQuery(
|
|
{ q: debouncedQ, page: 1, limit },
|
|
{
|
|
skip: !debouncedQ || debouncedQ.trim().length < 2,
|
|
refetchOnMountOrArgChange: true,
|
|
refetchOnFocus: true,
|
|
}
|
|
);
|
|
|
|
const results = useMemo(() => data?.results ?? [], [data?.results]);
|
|
|
|
const [expandedIds, setExpandedIds] = useState<Record<string, boolean>>({});
|
|
const toggleExpanded = useCallback((id: string) => {
|
|
setExpandedIds((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
}, []);
|
|
const isExpanded = useCallback(
|
|
(id: string) => !!expandedIds[id],
|
|
[expandedIds]
|
|
);
|
|
|
|
// Reset expansions when new query results arrive
|
|
useEffect(() => {
|
|
if (data?.results) setExpandedIds({});
|
|
}, [debouncedQ, data?.results]);
|
|
|
|
// Update route param only after debounce
|
|
useEffect(() => {
|
|
router.setParams({ q: debouncedQ || "" });
|
|
}, [debouncedQ]);
|
|
|
|
// Preprocess heavy formatting outside render
|
|
const formattedResults = useMemo(
|
|
() =>
|
|
results.map((item) => ({
|
|
...item,
|
|
subtitle: item.subtitle ? normalizeEnumLike(item.subtitle) : null,
|
|
description: item.description
|
|
? normalizeEnumLike(item.description)
|
|
: null,
|
|
createdAtFormatted: item.createdAt
|
|
? new Date(item.createdAt).toLocaleString()
|
|
: null,
|
|
})),
|
|
[results]
|
|
);
|
|
|
|
const renderResult = useCallback(
|
|
({ item }: { item: SearchResult }) => (
|
|
<ResultRow
|
|
item={item}
|
|
expanded={isExpanded(item.id)}
|
|
onToggle={toggleExpanded}
|
|
/>
|
|
),
|
|
[isExpanded, toggleExpanded]
|
|
);
|
|
|
|
return (
|
|
<View className="flex-1 p-4">
|
|
<View>
|
|
{isMobile && (
|
|
<Button
|
|
onPress={() => router.push("/dashboard/user-dashboard")}
|
|
disabled={isFetching}
|
|
className="mb-2"
|
|
>
|
|
Go to User Dashboard
|
|
</Button>
|
|
)}
|
|
<SearchBar
|
|
searchQuery={q}
|
|
onChange={setQ}
|
|
placeholderText="Search"
|
|
size={SizeEnum.Regular}
|
|
/>
|
|
</View>
|
|
|
|
{!q || q.trim().length < 2 ? (
|
|
<View className="mt-6">
|
|
<Text className="text-gray-600">Start typing to search.</Text>
|
|
</View>
|
|
) : isLoading && results.length === 0 ? (
|
|
<View className="mt-6">
|
|
<ActivityIndicator />
|
|
</View>
|
|
) : isError && results.length === 0 ? (
|
|
<View className="mt-6">
|
|
<Text className="text-red-500">Search failed. Please try again.</Text>
|
|
</View>
|
|
) : (
|
|
<View className="flex-1">
|
|
<Text className="text-gray-700 mb-2 mt-2">
|
|
Results: {results.length}
|
|
</Text>
|
|
<FlatList
|
|
data={formattedResults as SearchResult[]}
|
|
keyExtractor={(item) => `${item.type}-${item.id}`}
|
|
renderItem={renderResult}
|
|
contentContainerStyle={{ paddingBottom: 16 }}
|
|
ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
|
|
/>
|
|
{isFetching && (
|
|
<View className="my-3">
|
|
<ActivityIndicator />
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default SearchPage;
|