diff --git a/package-lock.json b/package-lock.json index 80152b61e..a31ee96dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "linkifyjs": "^4.0.2", "lodash": "^4.17.21", "markdown-it": "^13.0.1", - "nativewind": "^2.0.10", + "nativewind": "^2.0.11", "radio-buttons-react-native": "^1.0.4", "react": "18.1.0", "react-dom": "18.1.0", diff --git a/package.json b/package.json index bb9414494..c5dc3dc47 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "linkifyjs": "^4.0.2", "lodash": "^4.17.21", "markdown-it": "^13.0.1", - "nativewind": "^2.0.10", + "nativewind": "^2.0.11", "radio-buttons-react-native": "^1.0.4", "react": "18.1.0", "react-dom": "18.1.0", diff --git a/src/api/observations.js b/src/api/observations.js index d1a0ff92e..ec5bf85ff 100644 --- a/src/api/observations.js +++ b/src/api/observations.js @@ -76,9 +76,9 @@ const REMOTE_OBSERVATION_PARAMS = { const searchObservations = async ( params: Object = {}, opts: Object = {} ): Promise => { try { const { results } = await inatjs.observations.search( { ...PARAMS, ...params }, opts ); - return results; + return results || []; } catch ( e ) { - return handleError( e ); + return handleError( e, { throw: true } ); } }; @@ -172,10 +172,23 @@ const createOrUpdateEvidence = async ( } }; +const fetchObservationUpdates = async ( + params: Object = {}, + opts: Object = {} +): Promise => { + try { + const { results } = await inatjs.observations.updates( params, opts ); + return results; + } catch ( e ) { + return handleError( e, { throw: true } ); + } +}; + export { createObservation, createOrUpdateEvidence, faveObservation, + fetchObservationUpdates, fetchRemoteObservation, markAsReviewed, markObservationUpdatesViewed, diff --git a/src/components/Explore/BottomCard.js b/src/components/Explore/BottomCard.js deleted file mode 100644 index 9016accba..000000000 --- a/src/components/Explore/BottomCard.js +++ /dev/null @@ -1,63 +0,0 @@ -// @flow - -import { Text, View } from "components/styledComponents"; -import { t } from "i18next"; -import { ExploreContext } from "providers/contexts"; -import type { Node } from "react"; -import React, { useContext } from "react"; -import { viewStyles } from "styles/explore/explore"; - -import DropdownPicker from "./DropdownPicker"; -import FiltersIcon from "./FiltersIcon"; - -const Explore = ( ): Node => { - const { - exploreFilters, - setExploreFilters, - taxon, - setTaxon, - location, - setLocation - } = useContext( ExploreContext ); - const setTaxonId = getValue => { - setExploreFilters( { - ...exploreFilters, - taxon_id: getValue( ) - } ); - }; - - const setPlaceId = getValue => { - setExploreFilters( { - ...exploreFilters, - place_id: getValue( ) - } ); - }; - - const taxonId = exploreFilters ? exploreFilters.taxon_id : null; - const placeId = exploreFilters ? exploreFilters.place_id : null; - - return ( - - {t( "Explore" )} - - - - - ); -}; - -export default Explore; diff --git a/src/components/Explore/DropdownPicker.js b/src/components/Explore/DropdownPicker.js deleted file mode 100644 index 99c68304d..000000000 --- a/src/components/Explore/DropdownPicker.js +++ /dev/null @@ -1,129 +0,0 @@ -// flow - -import fetchSearchResults from "api/search"; -import { t } from "i18next"; -import type { Node } from "react"; -import React from "react"; -import { Image } from "react-native"; -// TODO: we'll probably need a custom dropdown picker which looks like a search bar -// and allows users to input immediately instead of first tapping the dropdown -// this is a placeholder to get functionality working -import DropDownPicker from "react-native-dropdown-picker"; -import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery"; -import { imageStyles, viewStyles } from "styles/explore/explore"; -import { useDebounce } from "use-debounce"; - -type Props = { - searchQuery: string, - setSearchQuery: string => { }, - setValue: number => { }, - sources: string, - value: number, - placeholder: string, - open: boolean, - onOpen: Function, - onClose: Function, - zIndex: number, - zIndexInverse: number -} - -const DropdownPicker = ( { - searchQuery, - setSearchQuery, - setValue, - sources, - value, - placeholder, - open, - onOpen, - onClose, - zIndex, - zIndexInverse -}: Props ): Node => { - // So we'll start searching only once the user finished typing - const [finalSearch] = useDebounce( searchQuery, 500 ); - - const { - data: searchResults - } = useAuthenticatedQuery( - ["fetchSearchResults", finalSearch], - optsWithAuth => fetchSearchResults( { - q: finalSearch, - sources - }, optsWithAuth ) - ); - - const placesItem = place => ( { - label: place.name, - value: place.uuid - } ); - - const taxonIcon = taxa => ( - - ); - const userIcon = user => ( - - ); - const projectIcon = project => ( - - ); - - const taxonItem = taxa => ( { - // TODO: match styling on the web; only show matched_term if the common name isn't clearly - // linked to the search result - label: `${taxa.preferred_common_name} (${taxa.matched_term})`, - value: taxa.id, - icon: taxonIcon( taxa ) - } ); - const userItem = user => ( { - label: user.login, - value: user.id, - icon: userIcon( user ) - } ); - - const projectItem = project => ( { - label: project.title, - value: project.id, - icon: projectIcon( project ) - } ); - - const displayItems = ( ) => { - if ( finalSearch === "" ) { - return []; - } - if ( !searchResults ) { return []; } - if ( sources === "places" ) { - return searchResults.map( item => placesItem( item ) ); - } if ( sources === "taxa" ) { - return searchResults.map( item => taxonItem( item ) ); - } if ( sources === "users" ) { - return searchResults.map( item => userItem( item ) ); - } if ( sources === "projects" ) { - return searchResults.map( item => projectItem( item ) ); - } - return []; - }; - - // TODO: change to the same style of dropdown as in SettingsRelationships? - // this should be standardized throughout the app - - return ( - - ); -}; - -export default DropdownPicker; diff --git a/src/components/Explore/Explore.js b/src/components/Explore/Explore.js index b7a9e9784..571af333d 100644 --- a/src/components/Explore/Explore.js +++ b/src/components/Explore/Explore.js @@ -1,45 +1,14 @@ // @flow -import ObservationViews from "components/Observations/ObservationViews"; +import PlaceholderText from "components/PlaceholderText"; import ViewWithFooter from "components/SharedComponents/ViewWithFooter"; -import { ExploreContext } from "providers/contexts"; import type { Node } from "react"; -import React, { useContext } from "react"; -import { Dimensions } from "react-native"; +import React from "react"; -import BottomCard from "./BottomCard"; - -const { height } = Dimensions.get( "screen" ); - -// make map small enough to show bottom card -const mapHeight = height - 450; - -const Explore = ( ): Node => { - const { - exploreList, - loadingExplore, - exploreFilters - } = useContext( ExploreContext ); - const taxonId = exploreFilters ? exploreFilters.taxon_id : null; - - return ( - - {taxonId !== null && ( - - )} - - - ); -}; +const Explore = ( ): Node => ( + + + +); export default Explore; diff --git a/src/components/Explore/ExploreFilters.js b/src/components/Explore/ExploreFilters.js deleted file mode 100644 index 451ce8b4d..000000000 --- a/src/components/Explore/ExploreFilters.js +++ /dev/null @@ -1,434 +0,0 @@ -// @flow - -import CheckBox from "@react-native-community/checkbox"; -import InputField from "components/SharedComponents/InputField"; -import ScrollNoFooter from "components/SharedComponents/ScrollNoFooter"; -import { Text, View } from "components/styledComponents"; -import { t } from "i18next"; -import { ExploreContext } from "providers/contexts"; -import RadioButtonRN from "radio-buttons-react-native"; -import type { Node } from "react"; -import React, { useContext, useState } from "react"; -import RNPickerSelect from "react-native-picker-select"; -import { pickerSelectStyles, viewStyles } from "styles/explore/exploreFilters"; - -import DropdownPicker from "./DropdownPicker"; -import ExploreFooter from "./ExploreFooter"; -import ResetFiltersButton from "./ResetFiltersButton"; -import TaxonLocationSearch from "./TaxonLocationSearch"; - -const ExploreFilters = ( ): Node => { - const [project, setProject] = useState( "" ); - const [user, setUser] = useState( "" ); - const { - exploreFilters, - setExploreFilters, - unappliedFilters, - setUnappliedFilters - } = useContext( ExploreContext ); - - const setProjectId = getValue => { - setUnappliedFilters( { - ...unappliedFilters, - project_id: getValue( ) - } ); - }; - - const setUserId = getValue => { - setUnappliedFilters( { - ...unappliedFilters, - user_id: getValue( ) - } ); - }; - - const sortByRadioButtons = [{ - label: t( "Date-added-newest-to-oldest" ), - type: "desc" - }, { - label: t( "Date-added-oldest-to-newest" ), - type: "asc" - }, { - label: t( "Recently-observed" ), - type: "observed_on" - }, { - label: t( "Most-faved" ), - type: "votes" - }]; - - const reviewedRadioButtons = [{ - label: t( "All-observations" ), - type: "all" - }, { - label: t( "Reviewed-only" ), - type: "reviewed" - }, { - label: t( "Unreviewed-only" ), - type: "unreviewed" - }]; - - const months = [ - { label: t( "Month-January" ), value: 1 }, - { label: t( "Month-February" ), value: 2 }, - { label: t( "Month-March" ), value: 3 }, - { label: t( "Month-April" ), value: 4 }, - { label: t( "Month-May" ), value: 5 }, - { label: t( "Month-June" ), value: 6 }, - { label: t( "Month-July" ), value: 7 }, - { label: t( "Month-August" ), value: 8 }, - { label: t( "Month-September" ), value: 9 }, - { label: t( "Month-October" ), value: 10 }, - { label: t( "Month-November" ), value: 11 }, - { label: t( "Month-December" ), value: 12 } - ]; - - const photoLicenses = [ - { label: t( "All" ), value: "all" }, - { label: "CC-BY", value: "cc-by" }, - { label: "CC-BY-NC", value: "cc-by-nc" }, - { label: "CC-BY-ND", value: "cc-by-nd" }, - { label: "CC-BY-SA", value: "cc-by-sa" }, - { label: "CC-BY-NC-ND", value: "cc-by-nc-nd" }, - { label: "CC-BY-NC-SA", value: "cc-by-nc-sa" }, - { label: "CC0", value: "cc0" } - ]; - - const ranks = [ - { label: t( "Ranks-stateofmatter" ), value: "stateofmatter" }, - { label: t( "Ranks-kingdom" ), value: "kingdom" }, - { label: t( "Ranks-subkingdom" ), value: "subkingdom" }, - { label: t( "Ranks-phylum" ), value: "phylum" }, - { label: t( "Ranks-subphylum" ), value: "subphylum" }, - { label: t( "Ranks-superclass" ), value: "superclass" }, - { label: t( "Ranks-class" ), value: "class" }, - { label: t( "Ranks-subclass" ), value: "subclass" }, - { label: t( "Ranks-infraclass" ), value: "infraclass" }, - { label: t( "Ranks-superorder" ), value: "superorder" }, - { label: t( "Ranks-order" ), value: "order" }, - { label: t( "Ranks-suborder" ), value: "suborder" }, - { label: t( "Ranks-infraorder" ), value: "infraorder" }, - { label: t( "Ranks-subterclass" ), value: "subterclass" }, - { label: t( "Ranks-parvorder" ), value: "parvorder" }, - { label: t( "Ranks-zoosection" ), value: "zoosection" }, - { label: t( "Ranks-zoosubsection" ), value: "zoosubsection" }, - { label: t( "Ranks-superfamily" ), value: "superfamily" }, - { label: t( "Ranks-epifamily" ), value: "epifamily" }, - { label: t( "Ranks-family" ), value: "family" }, - { label: t( "Ranks-subfamily" ), value: "subfamily" }, - { label: t( "Ranks-supertribe" ), value: "supertribe" }, - { label: t( "Ranks-tribe" ), value: "tribe" }, - { label: t( "Ranks-subtribe" ), value: "subtribe" }, - { label: t( "Ranks-genus" ), value: "genus" }, - { label: t( "Ranks-genushybrid" ), value: "genushybrid" }, - { label: t( "Ranks-subgenus" ), value: "subgenus" }, - { label: t( "Ranks-section" ), value: "section" }, - { label: t( "Ranks-subsection" ), value: "subsection" }, - { label: t( "Ranks-complex" ), value: "complex" }, - { label: t( "Ranks-species" ), value: "species" }, - { label: t( "Ranks-hybrid" ), value: "hybrid" }, - { label: t( "Ranks-subspecies" ), value: "subspecies" }, - { label: t( "Ranks-variety" ), value: "variety" }, - { label: t( "Ranks-form" ), value: "form" }, - { label: t( "Ranks-infrahybrid" ), value: "infrahybrid" } - ]; - - const projectId = unappliedFilters ? unappliedFilters.project_id : null; - const userId = unappliedFilters ? unappliedFilters.user_id : null; - - const renderQualityGradeCheckbox = qualityGrade => { - const filter = unappliedFilters.quality_grade; - const hasFilter = filter.includes( qualityGrade ); - - return ( - { - if ( hasFilter ) { - setUnappliedFilters( { - ...unappliedFilters, - quality_grade: filter.filter( e => e !== qualityGrade ) - } ); - } else { - filter.push( qualityGrade ); - setUnappliedFilters( { - ...unappliedFilters, - quality_grade: filter - } ); - } - }} - style={viewStyles.checkbox} - /> - ); - }; - - const renderMediaCheckbox = mediaType => { - const { sounds, photos } = unappliedFilters; - return ( - { - if ( mediaType === "photos" ) { - setUnappliedFilters( { - ...unappliedFilters, - photos: !unappliedFilters.photos - } ); - } else { - setUnappliedFilters( { - ...unappliedFilters, - sounds: !unappliedFilters.sounds - } ); - } - }} - style={viewStyles.checkbox} - /> - ); - }; - - const renderStatusCheckbox = status => { - const { - native, captive, introduced, threatened - } = unappliedFilters; - - let value; - - if ( status === "native" ) { - value = native; - } else if ( status === "captive" ) { - value = captive; - } else if ( status === "introduced" ) { - value = introduced; - } else { - value = threatened; - } - - return ( - { - setUnappliedFilters( { - ...unappliedFilters, - // $FlowFixMe - [status]: !unappliedFilters[status] - } ); - }} - style={viewStyles.checkbox} - /> - ); - }; - - const renderRankPicker = rank => ( - { - setUnappliedFilters( { - ...unappliedFilters, - // $FlowFixMe - [rank]: [itemValue] - } ); - }} - items={ranks} - useNativeAndroidPickerStyle={false} - style={pickerSelectStyles} - value={unappliedFilters[rank].length > 0 ? unappliedFilters[rank][0] : null} - /> - ); - - const renderMonthsPicker = ( ) => { - const firstMonth = unappliedFilters.months[0]; - const lastMonth = unappliedFilters.months[unappliedFilters.months.length - 1]; - - const includesMonth = value => unappliedFilters.months.includes( value ); - - const fillInMonths = itemValue => { - months.forEach( ( { value } ) => { - if ( value >= firstMonth && value <= itemValue && !includesMonth( value ) ) { - unappliedFilters.months.push( value ); - } else if ( value > itemValue && includesMonth( value ) ) { - const index = unappliedFilters.months.indexOf( value ); - unappliedFilters.months.splice( index ); - } - } ); - setUnappliedFilters( { ...unappliedFilters } ); - }; - - return ( - <> - { - unappliedFilters.months = [itemValue]; - setUnappliedFilters( { ...unappliedFilters } ); - }} - items={months} - useNativeAndroidPickerStyle={false} - style={pickerSelectStyles} - value={firstMonth} - /> - fillInMonths( itemValue )} - items={months} - useNativeAndroidPickerStyle={false} - style={pickerSelectStyles} - value={lastMonth} - /> - - ); - }; - - return ( - <> - - - {t( "Sort-by" )} - { - if ( type === "desc" || type === "asc" ) { - setExploreFilters( { - ...exploreFilters, - order: type, - order_by: "created_at" - } ); - } else { - // votes or observed_on only sort by most recent - setExploreFilters( { - ...exploreFilters, - order: "desc", - order_by: type - } ); - } - }} - /> - - {t( "Filters" )} - - - {t( "Quality-Grade" )} - - {renderQualityGradeCheckbox( "research" )} - {t( "Research-Grade" )} - - - {renderQualityGradeCheckbox( "needs_id" )} - {t( "Needs-ID" )} - - - {renderQualityGradeCheckbox( "casual" )} - {t( "Casual" )} - - {t( "User" )} - {t( "Search-for-a-user" )} - - {t( "Projects" )} - {t( "Search-for-a-project" )} - - {t( "Rank" )} - {t( "Low" )} - {renderRankPicker( "lrank" )} - {t( "High" )} - {renderRankPicker( "hrank" )} - {t( "Date" )} - {t( "Months" )} - {renderMonthsPicker( )} - {t( "Media" )} - - {renderMediaCheckbox( "photos" )} - {t( "Has-Photos" )} - - - {renderMediaCheckbox( "sounds" )} - {t( "Has-Sounds" )} - - {t( "Status" )} - - {renderStatusCheckbox( "introduced" )} - {t( "Introduced" )} - - - {renderStatusCheckbox( "native" )} - {t( "Native" )} - - - {renderStatusCheckbox( "threatened" )} - {t( "Threatened" )} - - - {renderStatusCheckbox( "captive" )} - {t( "Captive-Cultivated" )} - - {t( "Reviewed" )} - { - if ( type === "all" ) { - delete unappliedFilters.reviewed; - setUnappliedFilters( { ...unappliedFilters } ); - } else if ( type === "reviewed" ) { - setUnappliedFilters( { - ...unappliedFilters, - reviewed: true - } ); - } else { - setUnappliedFilters( { - ...unappliedFilters, - reviewed: false - } ); - } - }} - /> - {t( "Photo-Licensing" )} - { - setUnappliedFilters( { - ...unappliedFilters, - photo_license: itemValue === "all" ? [] : [itemValue] - } ); - }} - items={photoLicenses} - useNativeAndroidPickerStyle={false} - style={pickerSelectStyles} - value={ - unappliedFilters.photo_license.length > 0 - ? unappliedFilters.photo_license[0] - : "all" - } - /> - {t( "Description-Tags" )} - { - setUnappliedFilters( { - ...unappliedFilters, - q - } ); - }} - placeholder={t( "Search-for-description-tags-text" )} - text={unappliedFilters.q} - type="none" - /> - - - - - ); -}; - -export default ExploreFilters; diff --git a/src/components/Explore/ExploreFooter.js b/src/components/Explore/ExploreFooter.js deleted file mode 100644 index 9b464a547..000000000 --- a/src/components/Explore/ExploreFooter.js +++ /dev/null @@ -1,40 +0,0 @@ -// @flow - -import { HeaderBackButton } from "@react-navigation/elements"; -import { useNavigation } from "@react-navigation/native"; -import Button from "components/SharedComponents/Buttons/Button"; -import { t } from "i18next"; -import { ExploreContext } from "providers/contexts"; -import type { Node } from "react"; -import React from "react"; -import { View } from "react-native"; -import { viewStyles } from "styles/explore/exploreFilters"; - -const ExploreFooter = ( ): Node => { - const { applyFilters, resetUnappliedFilters } = React.useContext( ExploreContext ); - const navigation = useNavigation( ); - - const applyFiltersAndNavigate = ( ) => { - applyFilters( ); - navigation.goBack( ); - }; - - const clearFiltersAndNavigate = ( ) => { - resetUnappliedFilters( ); - navigation.goBack( ); - }; - - return ( - - -