diff --git a/dev-client/__tests__/integration/__snapshots__/LocationDashboardScreen-test.tsx.snap b/dev-client/__tests__/integration/__snapshots__/LocationDashboardScreen-test.tsx.snap index dc2bb1e12..29fd5428b 100644 --- a/dev-client/__tests__/integration/__snapshots__/LocationDashboardScreen-test.tsx.snap +++ b/dev-client/__tests__/integration/__snapshots__/LocationDashboardScreen-test.tsx.snap @@ -1367,7 +1367,217 @@ exports[`renders correctly 1`] = ` }, ] } - /> + > + + + + < /> + + + Soil ID + + + + + + Top Match + : + + + Loading… + + + + + Ecological Site Name + : + + + Loading… + + + + + + + Explore the data + + + + + + + + diff --git a/dev-client/src/components/SoilIdStatusDisplay.tsx b/dev-client/src/components/SoilIdStatusDisplay.tsx new file mode 100644 index 000000000..c557b9661 --- /dev/null +++ b/dev-client/src/components/SoilIdStatusDisplay.tsx @@ -0,0 +1,46 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import {ReactNode} from 'react'; + +import {SoilIdStatus} from 'terraso-client-shared/soilId/soilIdTypes'; + +export type SoilIdStatusDisplayProps = { + status: SoilIdStatus; + + loading: ReactNode; + error: ReactNode; + noData: ReactNode; + data: ReactNode; +}; +export const SoilIdStatusDisplay = ({ + status, + loading, + error, + noData, + data, +}: SoilIdStatusDisplayProps) => { + if (status === 'loading') { + return loading; + } else if (status === 'error') { + return error; + } else if (status === 'DATA_UNAVAILABLE') { + return noData; + } else { + return data; + } +}; diff --git a/dev-client/src/components/messages/AlertMessageBox.tsx b/dev-client/src/components/messages/AlertMessageBox.tsx new file mode 100644 index 000000000..aeb57ae64 --- /dev/null +++ b/dev-client/src/components/messages/AlertMessageBox.tsx @@ -0,0 +1,49 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import {Icon} from 'terraso-mobile-client/components/icons/Icon'; +import { + Box, + Column, + Row, + Text, +} from 'terraso-mobile-client/components/NativeBaseAdapters'; + +type MessageBoxProps = React.PropsWithChildren<{ + title?: string; +}>; + +export const AlertMessageBox = ({title, children}: MessageBoxProps) => { + return ( + + + + + + {title} + + {children} + + + + ); +}; diff --git a/dev-client/src/components/messages/ErrorMessageBox.tsx b/dev-client/src/components/messages/ErrorMessageBox.tsx new file mode 100644 index 000000000..dcd9866ad --- /dev/null +++ b/dev-client/src/components/messages/ErrorMessageBox.tsx @@ -0,0 +1,49 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import {Icon} from 'terraso-mobile-client/components/icons/Icon'; +import { + Box, + Column, + Row, + Text, +} from 'terraso-mobile-client/components/NativeBaseAdapters'; + +type MessageBoxProps = React.PropsWithChildren<{ + title?: string; +}>; + +export const ErrorMessageBox = ({title, children}: MessageBoxProps) => { + return ( + + + + + + {title} + + {children} + + + + ); +}; diff --git a/dev-client/src/model/soilId/soilIdTypes.ts b/dev-client/src/model/soilId/soilIdTypes.ts new file mode 100644 index 000000000..dff69d558 --- /dev/null +++ b/dev-client/src/model/soilId/soilIdTypes.ts @@ -0,0 +1,24 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { + SoilIdResults, + SoilIdStatus, +} from 'terraso-client-shared/soilId/soilIdSlice'; + +// TODO: This could be moved to client-shared +export type SoilIdData = SoilIdResults & {status: SoilIdStatus}; diff --git a/dev-client/src/screens/HomeScreen/components/CalloutDetail.tsx b/dev-client/src/screens/HomeScreen/components/CalloutDetail.tsx index f0c04e4e1..b89616199 100644 --- a/dev-client/src/screens/HomeScreen/components/CalloutDetail.tsx +++ b/dev-client/src/screens/HomeScreen/components/CalloutDetail.tsx @@ -21,14 +21,12 @@ export const CalloutDetail = ({ value, }: { label: string; - value: string; + value: React.ReactNode; }) => { return ( - + {label} - - {value} - + {value} ); }; diff --git a/dev-client/src/screens/HomeScreen/components/TemporaryLocationCallout.tsx b/dev-client/src/screens/HomeScreen/components/TemporaryLocationCallout.tsx index 3b7d49cdd..f1066f146 100644 --- a/dev-client/src/screens/HomeScreen/components/TemporaryLocationCallout.tsx +++ b/dev-client/src/screens/HomeScreen/components/TemporaryLocationCallout.tsx @@ -19,9 +19,15 @@ import {useCallback, useMemo} from 'react'; import {useTranslation} from 'react-i18next'; import {ActivityIndicator, Divider} from 'react-native-paper'; +import {TFunction} from 'i18next'; import {Button} from 'native-base'; +import { + DataBasedSoilMatch, + LocationBasedSoilMatch, +} from 'terraso-client-shared/graphqlSchema/graphql'; import {useSoilIdData} from 'terraso-client-shared/soilId/soilIdHooks'; +import {SoilIdStatus} from 'terraso-client-shared/soilId/soilIdSlice'; import {Coords} from 'terraso-client-shared/types'; import {CloseButton} from 'terraso-mobile-client/components/buttons/CloseButton'; @@ -30,7 +36,9 @@ import { Box, Column, Row, + Text, } from 'terraso-mobile-client/components/NativeBaseAdapters'; +import {SoilIdStatusDisplay} from 'terraso-mobile-client/components/SoilIdStatusDisplay'; import {renderElevation} from 'terraso-mobile-client/components/util/site'; import {useElevationData} from 'terraso-mobile-client/hooks/useElevationData'; import {getTopMatch} from 'terraso-mobile-client/model/soilId/soilIdRanking'; @@ -54,7 +62,6 @@ export const TemporaryLocationCallout = ({ const elevation = useElevationData(coords); const soilIdData = useSoilIdData(coords); - const isSoilIdReady = soilIdData.status === 'ready'; const topSoilMatch = useMemo(() => getTopMatch(soilIdData), [soilIdData]); const onCreate = useCallback(() => { @@ -80,32 +87,40 @@ export const TemporaryLocationCallout = ({ buttons={} isPopover={true}> - {!isSoilIdReady && } - {topSoilMatch && ( - <> - - - - - - )} - {elevation ? ( + <> + + } + /> + + } /> - ) : ( - - )} + + + {renderElevation(t, elevation)} + ) : ( + + ) + } + /> - - ); -}; - export const LocationDashboardContent = ({ siteId, coords, @@ -134,9 +79,6 @@ export const LocationDashboardContent = ({ : state.project.projects[site.projectId], ); - const soilIdData = useSoilIdData(coords, siteId); - const topSoilMatch = useMemo(() => getTopMatch(soilIdData), [soilIdData]); - const onExploreDataPress = useCallback(() => { navigation.navigate('LOCATION_SOIL_ID', {siteId, coords}); }, [navigation, siteId, coords]); @@ -217,17 +159,12 @@ export const LocationDashboardContent = ({ )} - {topSoilMatch && ( - - )} + ); diff --git a/dev-client/src/screens/LocationScreens/LocationSoilIdScreen.tsx b/dev-client/src/screens/LocationScreens/LocationSoilIdScreen.tsx index e7ea6695a..bc4236515 100644 --- a/dev-client/src/screens/LocationScreens/LocationSoilIdScreen.tsx +++ b/dev-client/src/screens/LocationScreens/LocationSoilIdScreen.tsx @@ -19,7 +19,6 @@ import {useTranslation} from 'react-i18next'; import {ScrollView} from 'react-native-gesture-handler'; import {selectSite} from 'terraso-client-shared/selectors'; -import {useSoilIdData} from 'terraso-client-shared/soilId/soilIdHooks'; import {Coords} from 'terraso-client-shared/types'; import {Box} from 'terraso-mobile-client/components/NativeBaseAdapters'; @@ -42,7 +41,6 @@ export const LocationSoilIdScreen = ({siteId, coords}: Props) => { const site = useSelector(state => siteId === undefined ? undefined : selectSite(siteId)(state), ); - const {status} = useSoilIdData(coords, siteId); return ( { }> - {status === 'ready' && ( - - )} + {siteId ? ( diff --git a/dev-client/src/screens/LocationScreens/components/LocationPrediction.tsx b/dev-client/src/screens/LocationScreens/components/LocationPrediction.tsx new file mode 100644 index 000000000..b97e064e4 --- /dev/null +++ b/dev-client/src/screens/LocationScreens/components/LocationPrediction.tsx @@ -0,0 +1,147 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import {useMemo} from 'react'; +import {useTranslation} from 'react-i18next'; + +import {TFunction} from 'i18next'; +import {Button} from 'native-base'; + +import { + DataBasedSoilMatch, + LocationBasedSoilMatch, +} from 'terraso-client-shared/graphqlSchema/graphql'; +import {useSoilIdData} from 'terraso-client-shared/soilId/soilIdHooks'; +import {SoilIdStatus} from 'terraso-client-shared/soilId/soilIdSlice'; +import {Coords} from 'terraso-client-shared/types'; + +import StackedBarChart from 'terraso-mobile-client/assets/stacked-bar.svg'; +import {Icon} from 'terraso-mobile-client/components/icons/Icon'; +import { + Box, + Row, + Text, +} from 'terraso-mobile-client/components/NativeBaseAdapters'; +import {SoilIdStatusDisplay} from 'terraso-mobile-client/components/SoilIdStatusDisplay'; +import {getTopMatch} from 'terraso-mobile-client/model/soilId/soilIdRanking'; + +type LocationPredictionProps = { + label: string; + coords: Coords; + siteId?: string; + onExploreDataPress: () => void; +}; + +export const LocationPrediction = ({ + label, + coords, + siteId, + onExploreDataPress, +}: LocationPredictionProps) => { + const {t} = useTranslation(); + + const soilIdData = useSoilIdData(coords, siteId); + const topSoilMatch = useMemo(() => getTopMatch(soilIdData), [soilIdData]); + + return ( + + + + + + + {label} + + + + + {t('soil.top_match')}: + + + + {t('soil.ecological_site_name')}: + + + + + + ); +}; + +type SoilIdStatusDisplayTopMatchProps = { + status: SoilIdStatus; + topSoilMatch: LocationBasedSoilMatch | DataBasedSoilMatch | undefined; + t: TFunction; +}; + +const TopSoilMatchDisplay = ({ + status, + topSoilMatch, + t, +}: SoilIdStatusDisplayTopMatchProps) => { + return ( + {t('soil.loading')}} + error={{t('soil.error')}} + noData={{t('soil.no_matches')}} + data={ + + {topSoilMatch?.soilInfo.soilSeries.name ?? t('soil.no_matches')} + + } + /> + ); +}; + +const EcologicalSiteMatchDisplay = ({ + status, + topSoilMatch, + t, +}: SoilIdStatusDisplayTopMatchProps) => { + return ( + {t('soil.loading')}} + error={{t('soil.error')}} + noData={{t('soil.no_matches')}} + data={ + + {topSoilMatch?.soilInfo.ecologicalSite?.name ?? t('soil.no_matches')} + + } + /> + ); +}; diff --git a/dev-client/src/screens/LocationScreens/components/soilId/SoilIdMatchesSection.tsx b/dev-client/src/screens/LocationScreens/components/soilId/SoilIdMatchesSection.tsx index bfc1697d3..c2299cc89 100644 --- a/dev-client/src/screens/LocationScreens/components/soilId/SoilIdMatchesSection.tsx +++ b/dev-client/src/screens/LocationScreens/components/soilId/SoilIdMatchesSection.tsx @@ -16,14 +16,20 @@ */ import {useTranslation} from 'react-i18next'; +import {ActivityIndicator} from 'react-native-paper'; import {useSoilIdData} from 'terraso-client-shared/soilId/soilIdHooks'; import {Coords} from 'terraso-client-shared/types'; import {ScreenContentSection} from 'terraso-mobile-client/components/content/ScreenContentSection'; +import {ExternalLink} from 'terraso-mobile-client/components/links/ExternalLink'; +import {AlertMessageBox} from 'terraso-mobile-client/components/messages/AlertMessageBox'; +import {ErrorMessageBox} from 'terraso-mobile-client/components/messages/ErrorMessageBox'; import { + Box, Heading, Row, + Text, } from 'terraso-mobile-client/components/NativeBaseAdapters'; import {InfoOverlaySheet} from 'terraso-mobile-client/components/sheets/InfoOverlaySheet'; import {InfoOverlaySheetButton} from 'terraso-mobile-client/components/sheets/InfoOverlaySheetButton'; @@ -44,7 +50,6 @@ export const SoilIdMatchesSection = ({ }: SoilIdMatchesSectionProps) => { const {t} = useTranslation(); const isSite = !!siteId; - const soilIdData = useSoilIdData(coords, siteId); return ( @@ -54,38 +59,91 @@ export const SoilIdMatchesSection = ({ - {isSite - ? getSortedDataBasedMatches(soilIdData).map(dataMatch => ( - ( - - )}> - - - )) - : getSortedLocationBasedMatches(soilIdData).map(locationMatch => ( - ( - - )}> - - - ))} + ); }; + +const MatchTilesOrMessage = ({siteId, coords}: SoilIdMatchesSectionProps) => { + const {t} = useTranslation(); + + const soilIdData = useSoilIdData(coords, siteId); + const status = soilIdData.status; + const isSite = !!siteId; + + switch (status) { + case 'loading': + return ; + case 'ready': { + if (isSite) { + return getSortedDataBasedMatches(soilIdData).map(dataMatch => ( + ( + + )}> + + + )); + } else { + return getSortedLocationBasedMatches(soilIdData).map(locationMatch => ( + ( + + )}> + + + )); + } + } + case 'DATA_UNAVAILABLE': + return ( + + + + ); + case 'error': + case 'ALGORITHM_FAILURE': + default: + return ( + + + {t('site.soil_id.matches.error_generic_body')} + + + ); + } +}; + +const NoMapDataAlertMessageContent = () => { + const {t} = useTranslation(); + + return ( + + + {t('site.soil_id.matches.no_map_data_body')} + + + {t('site.soil_id.matches.native_lands_intro')} + + + + ); +}; diff --git a/dev-client/src/theme.ts b/dev-client/src/theme.ts index a528b3b75..7f3db1b50 100644 --- a/dev-client/src/theme.ts +++ b/dev-client/src/theme.ts @@ -55,6 +55,12 @@ export const theme = extendTheme({ error: { main: '#D32F2F', contrast: '#FFFFFF', + background: '#FDEDED', + content: '#5F2120', + }, + warning: { + main: '#ED6C02', + content: '#663C00', }, grey: { 200: '#EEEEEE', diff --git a/dev-client/src/translations/en.json b/dev-client/src/translations/en.json index 9b5302c4e..5821c588d 100644 --- a/dev-client/src/translations/en.json +++ b/dev-client/src/translations/en.json @@ -9,7 +9,7 @@ "bullet_4": "Project management" }, "next": { - "title": "What's next?", + "title": "What’s next?", "subtitle": "Future versions will include:", "bullet_1": "Offline capabilities", "bullet_2": "Data visualization enhancements", @@ -120,7 +120,14 @@ "temp_location": "The soils listed are the most likely to be found at this location based on USDA NRCS soil maps.\n\nCreate a site here and enter the recommended data to improve accuracy of the match scores. " } }, - "match": "match" + "match": "match", + "error_generic_title": "Can’t fetch soil map", + "error_generic_body": "LandPKS was unable to fetch the soil map for this location. Try again later.", + "no_map_data_title": "No soil map data", + "no_map_data_body": "There is no soil map data available for this site, so LandPKS cannot generate soil matches for this site. Data can still be collected for this site, and if soil map data becomes available in the future, soil matches will appear here.", + "native_lands_intro": "Soil resources for Native lands:", + "native_lands_link": "nativeland.info", + "native_lands_url": "https://nativeland.info/" }, "site_data": { "title": "Site Data", @@ -147,7 +154,6 @@ "eco_id_label": "Ecological Site ID: {{id}}", "land_class_label": "Land Capability Class: {{land}}", "data_source_label": "Data Source: {{source}}", - "no_matches": "None available", "location_url": "View location on soil web", "inside_map_label": "In soil map unit", "outside_map_label": "In adjacent map unit, {{distance}} {{units}} distance", @@ -348,7 +354,7 @@ "contributor_help": "Can view, add, and edit site data", "viewer_help": "Can view site data", "remove": "Remove from project", - "remove_help": "Remove the team member's access to this project, retaining any data contributed to sites", + "remove_help": "Remove the team member’s access to this project, retaining any data contributed to sites", "confirm_removal_title": "Remove from Project?", "confirm_removal_body": "This will remove the team member from the project, but will retain their contributed site data.", "confirm_removal_action": "Remove" @@ -541,6 +547,9 @@ "bedrock": "Bedrock", "top_match": "Top Match", "ecological_site_name": "Ecological Site Name", + "loading": "Loading…", + "no_matches": "No matches", + "error": "Error", "explore_data": "Explore the data", "soil_id": "Soil ID", "surface": "Soil Surface",