From 9a95a964c6c8dd655e1c2c005ce3791ead2401b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sn=C3=A6r=20Seljan=20=C3=9E=C3=B3roddsson?= <112904566+snaerseljan@users.noreply.github.com> Date: Tue, 21 May 2024 10:17:07 +0000 Subject: [PATCH] feat(native-app): Problem and No data UX related component (#14828) * Added general problem component and translation hook wrapper * Update problem component * Update problem components logic and add problem to document-details screen * Update apps/native/app/src/ui/lib/problem/problem.tsx rename to error Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Rename to withContainer --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/src/hooks/use-translate.ts | 14 ++ apps/native/app/src/messages/en.ts | 11 ++ apps/native/app/src/messages/is.ts | 12 +- .../document-detail/document-detail.tsx | 11 +- .../src/ui/lib/problem/problem-template.tsx | 138 ++++++++++++++++++ .../native/app/src/ui/lib/problem/problem.tsx | 118 +++++++++++++++ 6 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 apps/native/app/src/hooks/use-translate.ts create mode 100644 apps/native/app/src/ui/lib/problem/problem-template.tsx create mode 100644 apps/native/app/src/ui/lib/problem/problem.tsx diff --git a/apps/native/app/src/hooks/use-translate.ts b/apps/native/app/src/hooks/use-translate.ts new file mode 100644 index 000000000000..6c34568cd4c7 --- /dev/null +++ b/apps/native/app/src/hooks/use-translate.ts @@ -0,0 +1,14 @@ +import { useIntl } from 'react-intl' +import { TranslatedMessage } from '../messages' + +/** + * Helper hook to simplify translations in the app. + */ +export const useTranslate = () => { + const intl = useIntl() + + return (key: TranslatedMessage) => + intl.formatMessage({ + id: key, + }) +} diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index a3201adc413f..4858b037dc7e 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -504,4 +504,15 @@ export const en: TranslatedMessages = { // offline 'offline.title': 'No internet connection', 'offline.message': 'Information has not been updated.', + + // problem + 'problem.error.tag': 'Error', + 'problem.error.title': 'Service is temporarily down', + 'problem.error.message': 'Please try again later', + 'problem.noData.title': 'No data', + 'problem.noData.message': + 'If you believe you have data that should appear here, please contact service provider.', + 'problem.offline.title': 'No internet connection', + 'problem.offline.message': + 'An error occurred while communicating with the service provider', } diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index d3e916d0a74e..a1a6f3fb72bd 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -502,7 +502,17 @@ export const is = { 'airDiscount.emptyListDescription': 'Einungis íbúar landsbyggðarinnar sem eiga lögheimili fjarri höfuðborgarsvæðinu og eyjum eiga rétt á Loftbrú.', - // Offline + // offline 'offline.title': 'Ekkert netsamband', 'offline.message': 'Upplýsingar hafa ekki verið uppfærðar.', + + // problems + 'problem.error.tag': 'Villa', + 'problem.error.title': 'Þjónusta liggur tímabundið niðri', + 'problem.error.message': 'Vinsamlegast reyndu aftur síðar', + 'problem.noData.title': 'Engin gögn', + 'problem.noData.message': + 'Ef þú telur þig eiga gögn sem ættu að birtast hér, vinsamlegast hafðu samband við þjónustuaðila.', + 'problem.offline.title': 'Samband næst ekki', + 'problem.offline.message': 'Villa kom upp í samskiptum við þjónustuaðila', } diff --git a/apps/native/app/src/screens/document-detail/document-detail.tsx b/apps/native/app/src/screens/document-detail/document-detail.tsx index a39e4eb87c5d..ae696f46eeed 100644 --- a/apps/native/app/src/screens/document-detail/document-detail.tsx +++ b/apps/native/app/src/screens/document-detail/document-detail.tsx @@ -1,5 +1,6 @@ import { useApolloClient, useFragment_experimental } from '@apollo/client' -import { blue400, dynamicColor, Header, Loader, Typography } from '@ui' +import { blue400, dynamicColor, Header, Loader } from '@ui' +import { Problem } from '@ui/lib/problem/problem' import React, { useEffect, useRef, useState } from 'react' import { FormattedDate, useIntl } from 'react-intl' import { Animated, Platform, StyleSheet, View } from 'react-native' @@ -382,16 +383,14 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{ style={[ StyleSheet.absoluteFill, { - alignItems: 'center', justifyContent: 'center', - maxHeight: 300, + + maxHeight: 500, }, ]} > {error ? ( - - {intl.formatMessage({ id: 'licenseScanDetail.errorUnknown' })} - + ) : ( { + switch (variant) { + case 'warning': + return require('../../assets/icons/warning.png') + + case 'info': + return require('../../assets/icons/info.png') + } +} + +const getColorsByVariant = ( + variant: Variant, +): { + borderColor: Colors + tagBackgroundColor: Colors + tagColor: Colors +} => { + switch (variant) { + case 'error': + return { + borderColor: 'red200', + tagBackgroundColor: 'red100', + tagColor: 'red600', + } + + case 'info': + return { + borderColor: 'blue200', + tagBackgroundColor: 'blue100', + tagColor: 'blue400', + } + + case 'warning': + return { + borderColor: 'yellow400', + tagBackgroundColor: 'yellow300', + tagColor: 'dark400', + } + } +} + +const Host = styled.View<{ + borderColor: Colors + noContainer?: boolean +}>` + border-color: ${({ borderColor, theme }) => theme.color[borderColor]}; + border-width: 1px; + border-radius: 24px; + + justify-content: center; + align-items: center; + flex: 1; + row-gap: ${({ theme }) => theme.spacing[3]}px; + + padding: ${({ theme }) => theme.spacing[2]}px; + ${({ noContainer, theme }) => noContainer && `margin: ${theme.spacing[2]}px;`} +` + +const Tag = styled(Typography)<{ + backgroundColor: Colors + color?: Colors +}>` + background-color: ${({ backgroundColor, theme }) => + theme.color[backgroundColor]}; + padding: ${({ theme }) => theme.spacing[1]}px; + border-radius: ${({ theme }) => theme.border.radius.large}; + overflow: hidden; + ${({ color, theme }) => color && `color: ${theme.color[color]};`} +` + +const Icon = styled(Image)(({ theme }) => ({ + width: theme.spacing[3], + height: theme.spacing[3], +})) + +const Content = styled(View)` + align-items: center; + row-gap: ${({ theme }) => theme.spacing[1]}px; +` + +export const ProblemTemplate = ({ + variant, + title, + message, + showIcon, + tag, + withContainer, +}: ProblemTemplateProps) => { + const { borderColor, tagColor, tagBackgroundColor } = + getColorsByVariant(variant) + + return ( + + {tag && ( + + {tag} + + )} + {showIcon && } + + + {title} + + {message} + + + ) +} diff --git a/apps/native/app/src/ui/lib/problem/problem.tsx b/apps/native/app/src/ui/lib/problem/problem.tsx new file mode 100644 index 000000000000..40963f69836c --- /dev/null +++ b/apps/native/app/src/ui/lib/problem/problem.tsx @@ -0,0 +1,118 @@ +import { useEffect } from 'react' +import { useTranslate } from '../../../hooks/use-translate' +import { useOfflineStore } from '../../../stores/offline-store' +import { ProblemTemplate, ProblemTemplateBaseProps } from './problem-template' + +enum ProblemTypes { + error = 'error', + noData = 'no_data', +} + +type ProblemBaseProps = { + /** + * Type of problem + * @default 'error' + * 'error' is a generic error that is not caused by the user + * 'no_data' is a 200 response, i.e. no data + */ + type?: `${ProblemTypes}` + error?: Error + title?: string + message?: string + logError?: boolean +} & Pick + +interface ErrorProps extends ProblemBaseProps { + type?: 'error' + showIcon?: never + error?: Error + title?: string + message?: string + tag?: string +} + +interface NoDataBaseProps extends ProblemBaseProps { + type: 'no_data' + error?: never + title?: string + message?: string +} + +interface NoDataWithIconProps extends NoDataBaseProps { + showIcon?: boolean + tag?: never +} + +interface NoDataWithTagProps extends NoDataBaseProps { + showIcon?: never + tag?: string +} + +type NoDataProps = NoDataWithIconProps | NoDataWithTagProps + +type ProblemProps = ErrorProps | NoDataProps + +export const Problem = ({ + type = ProblemTypes.error, + error, + title, + message, + tag, + logError = false, + withContainer, + showIcon, +}: ProblemProps) => { + const t = useTranslate() + const { isConnected } = useOfflineStore() + + const defaultProps = { withContainer } + + const fallbackProps = { + ...defaultProps, + title: title ?? t('problem.error.title'), + message: message ?? t('problem.error.message'), + tag: tag ?? t('problem.error.tag'), + variant: 'error', + } as const + + useEffect(() => { + if (logError && error) { + console.error(error) + } + }, [logError, error]) + + // When offline prioritize showing offline template + if (!isConnected) { + return ( + + ) + } + + const noDataProps = + showIcon || !tag ? { showIcon: !tag ? true : showIcon } : { tag } + + switch (type) { + case ProblemTypes.error: + return + + case ProblemTypes.noData: + return ( + + ) + + default: + return + } +}