mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-06-21 14:08:31 -04:00
This commit is contained in:
committed by
GitHub
parent
62dce20bd9
commit
a0fd52ca27
45
src/components/Explore/SearchScreens/EmptySearchResults.tsx
Normal file
45
src/components/Explore/SearchScreens/EmptySearchResults.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Body2,
|
||||
OfflineNotice
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import React from "react";
|
||||
import {
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean,
|
||||
searchQuery: string,
|
||||
refetch: Function
|
||||
}
|
||||
|
||||
const EmptySearchResults = ( { isLoading, searchQuery, refetch }: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
|
||||
if ( searchQuery === "" ) {
|
||||
return null;
|
||||
}
|
||||
if ( isConnected === false ) {
|
||||
return (
|
||||
<View className="pt-[50px]">
|
||||
<OfflineNotice onPress={refetch} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<ActivityIndicator size={40} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Body2 className="text-center pt-[50px]">{t( "No-results-found-for-that-search" )}</Body2>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptySearchResults;
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import fetchSearchResults from "api/search";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Body3,
|
||||
Button,
|
||||
Heading4,
|
||||
@@ -26,6 +25,8 @@ import { useAuthenticatedQuery, useTranslation } from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
|
||||
import { getShadow } from "styles/global";
|
||||
|
||||
import EmptySearchResults from "./EmptySearchResults";
|
||||
|
||||
const DROP_SHADOW = getShadow( {
|
||||
offsetHeight: 4
|
||||
} );
|
||||
@@ -51,7 +52,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
|
||||
[updateLocation, closeModal]
|
||||
);
|
||||
|
||||
const { data: placeResults, isLoading } = useAuthenticatedQuery(
|
||||
const { data: placeResults, isLoading, refetch } = useAuthenticatedQuery(
|
||||
["fetchSearchResults", locationName],
|
||||
optsWithAuth => fetchSearchResults(
|
||||
{
|
||||
@@ -114,11 +115,19 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
|
||||
}
|
||||
};
|
||||
|
||||
const renderEmptyList = ( ) => (
|
||||
<EmptySearchResults
|
||||
isLoading={isLoading}
|
||||
searchQuery={locationName}
|
||||
refetch={refetch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper testID="explore-location-search">
|
||||
<View className="flex-row justify-center p-5 bg-white">
|
||||
<INatIconButton
|
||||
testID="ExploreTaxonSearch.close"
|
||||
testID="ExploreLocationSearch.close"
|
||||
size={18}
|
||||
icon="back"
|
||||
className="absolute top-2 left-3 z-10"
|
||||
@@ -138,7 +147,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
|
||||
<SearchBar
|
||||
handleTextChange={locationText => setLocationName( locationText )}
|
||||
value={locationName}
|
||||
testID="LocationPicker.locationSearch"
|
||||
testID="ExploreLocationSearch.locationSearch"
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-row px-3 mt-5 justify-evenly">
|
||||
@@ -155,20 +164,13 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{isLoading
|
||||
? (
|
||||
<View className="p-4">
|
||||
<ActivityIndicator size={40} />
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
/>
|
||||
)}
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
ListEmptyComponent={renderEmptyList}
|
||||
/>
|
||||
{renderPermissionsGate( { onPermissionGranted: setNearbyLocation } )}
|
||||
</ViewWrapper>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import { searchProjects } from "api/projects";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Body3,
|
||||
Heading4,
|
||||
INatIconButton,
|
||||
@@ -20,6 +19,8 @@ import React, {
|
||||
import { useAuthenticatedQuery, useTranslation } from "sharedHooks";
|
||||
import { getShadow } from "styles/global";
|
||||
|
||||
import EmptySearchResults from "./EmptySearchResults";
|
||||
|
||||
const DROP_SHADOW = getShadow( {
|
||||
offsetHeight: 4
|
||||
} );
|
||||
@@ -30,12 +31,12 @@ type Props = {
|
||||
};
|
||||
|
||||
const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => {
|
||||
const [userQuery, setUserQuery] = useState( "" );
|
||||
const [projectQuery, setProjectQuery] = useState( "" );
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: projects, isLoading } = useAuthenticatedQuery(
|
||||
["searchProjects", userQuery],
|
||||
optsWithAuth => searchProjects( { q: userQuery }, optsWithAuth )
|
||||
const { data: projects, isLoading, refetch } = useAuthenticatedQuery(
|
||||
["searchProjects", projectQuery],
|
||||
optsWithAuth => searchProjects( { q: projectQuery }, optsWithAuth )
|
||||
);
|
||||
|
||||
const onProjectSelected = useCallback( async project => {
|
||||
@@ -77,11 +78,19 @@ const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => {
|
||||
<View className="border-b border-lightGray" />
|
||||
);
|
||||
|
||||
const renderEmptyList = ( ) => (
|
||||
<EmptySearchResults
|
||||
isLoading={isLoading}
|
||||
searchQuery={projectQuery}
|
||||
refetch={refetch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<View className="flex-row justify-center p-5 bg-white">
|
||||
<INatIconButton
|
||||
testID="ExploreTaxonSearch.close"
|
||||
testID="ExploreProjectSearch.close"
|
||||
size={18}
|
||||
icon="back"
|
||||
className="absolute top-2 left-3 z-10"
|
||||
@@ -98,30 +107,23 @@ const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => {
|
||||
style={DROP_SHADOW}
|
||||
>
|
||||
<SearchBar
|
||||
handleTextChange={setUserQuery}
|
||||
value={userQuery}
|
||||
testID="SearchUser"
|
||||
handleTextChange={setProjectQuery}
|
||||
value={projectQuery}
|
||||
testID="SearchProject"
|
||||
/>
|
||||
</View>
|
||||
{isLoading
|
||||
? (
|
||||
<View className="p-4">
|
||||
<ActivityIndicator size={40} />
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<FlashList
|
||||
data={projects}
|
||||
initialNumToRender={5}
|
||||
estimatedItemSize={100}
|
||||
testID="SearchUserList"
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderItem}
|
||||
ListHeaderComponent={renderItemSeparator}
|
||||
ItemSeparatorComponent={renderItemSeparator}
|
||||
accessible
|
||||
/>
|
||||
)}
|
||||
<FlashList
|
||||
data={projects}
|
||||
initialNumToRender={5}
|
||||
estimatedItemSize={100}
|
||||
testID="SearchProjectList"
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={renderEmptyList}
|
||||
ListHeaderComponent={renderItemSeparator}
|
||||
ItemSeparatorComponent={renderItemSeparator}
|
||||
accessible
|
||||
/>
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import fetchSearchResults from "api/search";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Body3,
|
||||
Heading4,
|
||||
INatIconButton,
|
||||
@@ -20,6 +19,8 @@ import React, {
|
||||
import { useAuthenticatedQuery, useTranslation } from "sharedHooks";
|
||||
import { getShadow } from "styles/global";
|
||||
|
||||
import EmptySearchResults from "./EmptySearchResults";
|
||||
|
||||
const DROP_SHADOW = getShadow( {
|
||||
offsetHeight: 4
|
||||
} );
|
||||
@@ -34,7 +35,7 @@ const ExploreUserSearch = ( { closeModal, updateUser }: Props ): Node => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// TODO: replace this with infinite scroll like ExploreFlashList
|
||||
const { data: userList, isLoading } = useAuthenticatedQuery(
|
||||
const { data: userList, isLoading, refetch } = useAuthenticatedQuery(
|
||||
["fetchSearchResults", userQuery],
|
||||
optsWithAuth => fetchSearchResults(
|
||||
{
|
||||
@@ -82,11 +83,19 @@ const ExploreUserSearch = ( { closeModal, updateUser }: Props ): Node => {
|
||||
<View className="border-b border-lightGray" />
|
||||
);
|
||||
|
||||
const renderEmptyList = ( ) => (
|
||||
<EmptySearchResults
|
||||
isLoading={isLoading}
|
||||
searchQuery={userQuery}
|
||||
refetch={refetch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<View className="flex-row justify-center p-5 bg-white">
|
||||
<INatIconButton
|
||||
testID="ExploreTaxonSearch.close"
|
||||
testID="ExploreUserSearch.close"
|
||||
size={18}
|
||||
icon="back"
|
||||
className="absolute top-2 left-3 z-10"
|
||||
@@ -108,26 +117,19 @@ const ExploreUserSearch = ( { closeModal, updateUser }: Props ): Node => {
|
||||
testID="SearchUser"
|
||||
/>
|
||||
</View>
|
||||
{isLoading
|
||||
? (
|
||||
<View className="p-4">
|
||||
<ActivityIndicator size={40} />
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<FlashList
|
||||
ItemSeparatorComponent={renderItemSeparator}
|
||||
ListHeaderComponent={renderItemSeparator}
|
||||
accessible
|
||||
data={userList}
|
||||
estimatedItemSize={100}
|
||||
initialNumToRender={5}
|
||||
keyExtractor={item => item.id}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
renderItem={renderItem}
|
||||
testID="SearchUserList"
|
||||
/>
|
||||
)}
|
||||
<FlashList
|
||||
ItemSeparatorComponent={renderItemSeparator}
|
||||
ListEmptyComponent={renderEmptyList}
|
||||
ListHeaderComponent={renderItemSeparator}
|
||||
accessible
|
||||
data={userList}
|
||||
estimatedItemSize={100}
|
||||
initialNumToRender={5}
|
||||
keyExtractor={item => item.id}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
renderItem={renderItem}
|
||||
testID="SearchUserList"
|
||||
/>
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { refresh, useNetInfo } from "@react-native-community/netinfo";
|
||||
import { ActivityIndicator, OfflineNotice } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import EmptySearchResults from "components/Explore/SearchScreens/EmptySearchResults.tsx";
|
||||
import React from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { useIconicTaxa } from "sharedHooks";
|
||||
@@ -24,53 +22,30 @@ const TaxaList = ( {
|
||||
}: Props ) => {
|
||||
// TODO: how to use Realm with TS
|
||||
const iconicTaxa = useIconicTaxa( { reload: false } );
|
||||
const { isConnected } = useNetInfo( );
|
||||
|
||||
let data = iconicTaxa;
|
||||
if ( taxa && taxa.length > 0 ) {
|
||||
// TODO: how to use Realm with TS
|
||||
data = taxa;
|
||||
}
|
||||
// 20240816 amanda - afaik we only want to seed the initial screen
|
||||
// with iconic taxon data, and we still want to be able to show the empty screen
|
||||
// and offline state when a user has typed in a query
|
||||
const data = taxonQuery === ""
|
||||
? iconicTaxa
|
||||
: taxa;
|
||||
|
||||
const renderMainContent = () => {
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<ActivityIndicator size={40} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const showIfOffline = taxonQuery.length > 0 && (
|
||||
!taxa || ( taxa instanceof Array && taxa.length === 0 )
|
||||
);
|
||||
if ( showIfOffline && !isConnected ) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<OfflineNotice
|
||||
onPress={() => {
|
||||
refresh();
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderEmptyList = ( ) => (
|
||||
<EmptySearchResults
|
||||
isLoading={isLoading}
|
||||
searchQuery={taxonQuery}
|
||||
refetch={refetch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
{renderMainContent()}
|
||||
</View>
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
ListEmptyComponent={renderEmptyList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -603,6 +603,7 @@ No-Notifications-Found = You have no notifications! Get started by creating your
|
||||
No-projects-match-that-search = No projects match that search
|
||||
# Used for explore screen when search params lead to a search with no data
|
||||
No-results-found = No results found
|
||||
No-results-found-for-that-search = No results found for that search.
|
||||
# license code
|
||||
no-rights-reserved-cc0 = no rights reserved (CC0)
|
||||
NONE = NONE
|
||||
|
||||
@@ -812,6 +812,7 @@
|
||||
"comment": "Used for explore screen when search params lead to a search with no data",
|
||||
"val": "No results found"
|
||||
},
|
||||
"No-results-found-for-that-search": "No results found for that search.",
|
||||
"no-rights-reserved-cc0": {
|
||||
"comment": "license code",
|
||||
"val": "no rights reserved (CC0)"
|
||||
|
||||
@@ -603,6 +603,7 @@ No-Notifications-Found = You have no notifications! Get started by creating your
|
||||
No-projects-match-that-search = No projects match that search
|
||||
# Used for explore screen when search params lead to a search with no data
|
||||
No-results-found = No results found
|
||||
No-results-found-for-that-search = No results found for that search.
|
||||
# license code
|
||||
no-rights-reserved-cc0 = no rights reserved (CC0)
|
||||
NONE = NONE
|
||||
|
||||
Reference in New Issue
Block a user