Files
RN_Template/RN_TEMPLATE/app/components/EmptyState.tsx

250 lines
6.7 KiB
TypeScript
Raw Permalink Normal View History

2026-02-05 13:16:05 +08:00
import { Image, ImageProps, ImageStyle, StyleProp, TextStyle, View, ViewStyle } from "react-native"
import { useTranslation } from "react-i18next"
import { useAppTheme } from "@/theme/context"
import type { ThemedStyle } from "@/theme/types"
import { Button, ButtonProps } from "./Button"
import { Text, TextProps } from "./Text"
const sadFace = require("@assets/images/sad-face.png")
interface EmptyStateProps {
/**
* An optional prop that specifies the text/image set to use for the empty state.
*/
preset?: "generic"
/**
* Style override for the container.
*/
style?: StyleProp<ViewStyle>
/**
* An Image source to be displayed above the heading.
*/
imageSource?: ImageProps["source"]
/**
* Style overrides for image.
*/
imageStyle?: StyleProp<ImageStyle>
/**
* Pass any additional props directly to the Image component.
*/
ImageProps?: Omit<ImageProps, "source">
/**
* The heading text to display if not using `headingTx`.
*/
heading?: TextProps["text"]
/**
* Heading text which is looked up via i18n.
*/
headingTx?: TextProps["tx"]
/**
* Optional heading options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
headingTxOptions?: TextProps["txOptions"]
/**
* Style overrides for heading text.
*/
headingStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the heading Text component.
*/
HeadingTextProps?: TextProps
/**
* The content text to display if not using `contentTx`.
*/
content?: TextProps["text"]
/**
* Content text which is looked up via i18n.
*/
contentTx?: TextProps["tx"]
/**
* Optional content options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
contentTxOptions?: TextProps["txOptions"]
/**
* Style overrides for content text.
*/
contentStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the content Text component.
*/
ContentTextProps?: TextProps
/**
* The button text to display if not using `buttonTx`.
*/
button?: TextProps["text"]
/**
* Button text which is looked up via i18n.
*/
buttonTx?: TextProps["tx"]
/**
* Optional button options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
buttonTxOptions?: TextProps["txOptions"]
/**
* Style overrides for button.
*/
buttonStyle?: ButtonProps["style"]
/**
* Style overrides for button text.
*/
buttonTextStyle?: ButtonProps["textStyle"]
/**
* Called when the button is pressed.
*/
buttonOnPress?: ButtonProps["onPress"]
/**
* Pass any additional props directly to the Button component.
*/
ButtonProps?: ButtonProps
}
interface EmptyStatePresetItem {
imageSource: ImageProps["source"]
heading: TextProps["text"]
content: TextProps["text"]
button: TextProps["text"]
}
/**
* A component to use when there is no data to display. It can be utilized to direct the user what to do next.
* @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/EmptyState/}
* @param {EmptyStateProps} props - The props for the `EmptyState` component.
* @returns {JSX.Element} The rendered `EmptyState` component.
*/
export function EmptyState(props: EmptyStateProps) {
const {
theme,
themed,
theme: { spacing },
} = useAppTheme()
const { t } = useTranslation()
const EmptyStatePresets = {
generic: {
imageSource: sadFace,
heading: t("emptyStateComponent:generic.heading"),
content: t("emptyStateComponent:generic.content"),
button: t("emptyStateComponent:generic.button"),
} as EmptyStatePresetItem,
} as const
const preset = EmptyStatePresets[props.preset ?? "generic"]
const {
button = preset.button,
buttonTx,
buttonOnPress,
buttonTxOptions,
content = preset.content,
contentTx,
contentTxOptions,
heading = preset.heading,
headingTx,
headingTxOptions,
imageSource = preset.imageSource,
style: $containerStyleOverride,
buttonStyle: $buttonStyleOverride,
buttonTextStyle: $buttonTextStyleOverride,
contentStyle: $contentStyleOverride,
headingStyle: $headingStyleOverride,
imageStyle: $imageStyleOverride,
ButtonProps,
ContentTextProps,
HeadingTextProps,
ImageProps,
} = props
const isImagePresent = !!imageSource
const isHeadingPresent = !!(heading || headingTx)
const isContentPresent = !!(content || contentTx)
const isButtonPresent = !!(button || buttonTx)
const $containerStyles = [$containerStyleOverride]
const $imageStyles = [
$image,
(isHeadingPresent || isContentPresent || isButtonPresent) && { marginBottom: spacing.xxxs },
$imageStyleOverride,
ImageProps?.style,
]
const $headingStyles = [
themed($heading),
isImagePresent && { marginTop: spacing.xxxs },
(isContentPresent || isButtonPresent) && { marginBottom: spacing.xxxs },
$headingStyleOverride,
HeadingTextProps?.style,
]
const $contentStyles = [
themed($content),
(isImagePresent || isHeadingPresent) && { marginTop: spacing.xxxs },
isButtonPresent && { marginBottom: spacing.xxxs },
$contentStyleOverride,
ContentTextProps?.style,
]
const $buttonStyles = [
(isImagePresent || isHeadingPresent || isContentPresent) && { marginTop: spacing.xl },
$buttonStyleOverride,
ButtonProps?.style,
]
return (
<View style={$containerStyles}>
{isImagePresent && (
<Image
source={imageSource}
{...ImageProps}
style={$imageStyles}
tintColor={theme.colors.palette.neutral900}
/>
)}
{isHeadingPresent && (
<Text
preset="subheading"
text={heading}
tx={headingTx}
txOptions={headingTxOptions}
{...HeadingTextProps}
style={$headingStyles}
/>
)}
{isContentPresent && (
<Text
text={content}
tx={contentTx}
txOptions={contentTxOptions}
{...ContentTextProps}
style={$contentStyles}
/>
)}
{isButtonPresent && (
<Button
onPress={buttonOnPress}
text={button}
tx={buttonTx}
txOptions={buttonTxOptions}
textStyle={$buttonTextStyleOverride}
{...ButtonProps}
style={$buttonStyles}
/>
)}
</View>
)
}
const $image: ImageStyle = { alignSelf: "center" }
const $heading: ThemedStyle<TextStyle> = ({ spacing }) => ({
textAlign: "center",
paddingHorizontal: spacing.lg,
})
const $content: ThemedStyle<TextStyle> = ({ spacing }) => ({
textAlign: "center",
paddingHorizontal: spacing.lg,
})