Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(native-app): Problem and No data UX related component #14828

Merged
merged 6 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/native/app/src/hooks/use-translate.ts
Original file line number Diff line number Diff line change
@@ -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,
})
}
11 changes: 11 additions & 0 deletions apps/native/app/src/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
12 changes: 11 additions & 1 deletion apps/native/app/src/messages/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -382,16 +383,14 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
style={[
StyleSheet.absoluteFill,
{
alignItems: 'center',
justifyContent: 'center',
maxHeight: 300,

maxHeight: 500,
snaerseljan marked this conversation as resolved.
Show resolved Hide resolved
},
]}
>
{error ? (
<Typography>
{intl.formatMessage({ id: 'licenseScanDetail.errorUnknown' })}
</Typography>
<Problem type="error" withContainer />
) : (
<Loader
text={intl.formatMessage({ id: 'documentDetail.loadingText' })}
Expand Down
138 changes: 138 additions & 0 deletions apps/native/app/src/ui/lib/problem/problem-template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Colors, Typography } from '@ui'
import { ReactNode } from 'react'
import { Image, View } from 'react-native'
import styled from 'styled-components/native'

type Variant = 'info' | 'error' | 'warning'

export type ProblemTemplateBaseProps = {
variant: Variant
title: string
message: string | ReactNode
withContainer?: boolean
}

interface WithIconProps extends ProblemTemplateBaseProps {
showIcon?: boolean
tag?: never
}

interface WithTagProps extends ProblemTemplateBaseProps {
tag?: string
showIcon?: never
}

export type ProblemTemplateProps = WithIconProps | WithTagProps

const getIcon = (variant: Variant) => {
switch (variant) {
case 'warning':
return require('../../assets/icons/warning.png')

case 'info':
return require('../../assets/icons/info.png')
}
}
snaerseljan marked this conversation as resolved.
Show resolved Hide resolved

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 (
<Host borderColor={borderColor} noContainer={withContainer}>
{tag && (
<Tag
variant="eyebrow"
backgroundColor={tagBackgroundColor}
color={tagColor}
>
{tag}
</Tag>
)}
{showIcon && <Icon source={getIcon(variant)} />}
<Content>
<Typography variant="heading3" textAlign="center">
{title}
</Typography>
<Typography textAlign="center">{message}</Typography>
</Content>
</Host>
)
}
118 changes: 118 additions & 0 deletions apps/native/app/src/ui/lib/problem/problem.tsx
Original file line number Diff line number Diff line change
@@ -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<ProblemTemplateBaseProps, 'withContainer'>

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 (
<ProblemTemplate
{...defaultProps}
showIcon
variant="warning"
title={title ?? t('problem.offline.title')}
message={message ?? t('problem.offline.message')}
/>
)
}

const noDataProps =
showIcon || !tag ? { showIcon: !tag ? true : showIcon } : { tag }

switch (type) {
case ProblemTypes.error:
return <ProblemTemplate {...fallbackProps} />

case ProblemTypes.noData:
return (
<ProblemTemplate
{...defaultProps}
{...noDataProps}
variant="info"
title={title ?? t('problem.noData.title')}
message={message ?? t('problem.noData.message')}
/>
)

default:
return <ProblemTemplate {...fallbackProps} />
}
}
Loading