import { ComponentType, Fragment, ReactElement } from "react" import { StyleProp, TextStyle, TouchableOpacity, TouchableOpacityProps, View, ViewProps, ViewStyle, } from "react-native" import { useAppTheme } from "@/theme/context" import { $styles } from "@/theme/styles" import type { ThemedStyle, ThemedStyleArray } from "@/theme/types" import { s } from "@/utils/responsive" import { Text, TextProps } from "./Text" type Presets = "default" | "reversed" interface CardProps extends TouchableOpacityProps { /** * One of the different types of text presets. */ preset?: Presets /** * How the content should be aligned vertically. This is especially (but not exclusively) useful * when the card is a fixed height but the content is dynamic. * * `top` (default) - aligns all content to the top. * `center` - aligns all content to the center. * `space-between` - spreads out the content evenly. * `force-footer-bottom` - aligns all content to the top, but forces the footer to the bottom. */ verticalAlignment?: "top" | "center" | "space-between" | "force-footer-bottom" /** * Custom component added to the left of the card body. */ LeftComponent?: ReactElement /** * Custom component added to the right of the card body. */ RightComponent?: ReactElement /** * 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 /** * Pass any additional props directly to the heading Text component. */ HeadingTextProps?: TextProps /** * Custom heading component. * Overrides all other `heading*` props. */ HeadingComponent?: ReactElement /** * 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 /** * Pass any additional props directly to the content Text component. */ ContentTextProps?: TextProps /** * Custom content component. * Overrides all other `content*` props. */ ContentComponent?: ReactElement /** * The footer text to display if not using `footerTx`. */ footer?: TextProps["text"] /** * Footer text which is looked up via i18n. */ footerTx?: TextProps["tx"] /** * Optional footer options to pass to i18n. Useful for interpolation * as well as explicitly setting locale or translation fallbacks. */ footerTxOptions?: TextProps["txOptions"] /** * Style overrides for footer text. */ footerStyle?: StyleProp /** * Pass any additional props directly to the footer Text component. */ FooterTextProps?: TextProps /** * Custom footer component. * Overrides all other `footer*` props. */ FooterComponent?: ReactElement } /** * Cards are useful for displaying related information in a contained way. * If a ListItem displays content horizontally, a Card can be used to display content vertically. * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Card/} * @param {CardProps} props - The props for the `Card` component. * @returns {JSX.Element} The rendered `Card` component. */ export function Card(props: CardProps) { const { content, contentTx, contentTxOptions, footer, footerTx, footerTxOptions, heading, headingTx, headingTxOptions, ContentComponent, HeadingComponent, FooterComponent, LeftComponent, RightComponent, verticalAlignment = "top", style: $containerStyleOverride, contentStyle: $contentStyleOverride, headingStyle: $headingStyleOverride, footerStyle: $footerStyleOverride, ContentTextProps, HeadingTextProps, FooterTextProps, ...WrapperProps } = props const { themed, theme: { spacing }, } = useAppTheme() const preset: Presets = props.preset ?? "default" const isPressable = !!WrapperProps.onPress const isHeadingPresent = !!(HeadingComponent || heading || headingTx) const isContentPresent = !!(ContentComponent || content || contentTx) const isFooterPresent = !!(FooterComponent || footer || footerTx) const Wrapper = (isPressable ? TouchableOpacity : View) as ComponentType< TouchableOpacityProps | ViewProps > const HeaderContentWrapper = verticalAlignment === "force-footer-bottom" ? View : Fragment const $containerStyle: StyleProp = [ themed($containerPresets[preset]), $containerStyleOverride, ] const $headingStyle = [ themed($headingPresets[preset]), (isFooterPresent || isContentPresent) && { marginBottom: spacing.xxxs }, $headingStyleOverride, HeadingTextProps?.style, ] const $contentStyle = [ themed($contentPresets[preset]), isHeadingPresent && { marginTop: spacing.xxxs }, isFooterPresent && { marginBottom: spacing.xxxs }, $contentStyleOverride, ContentTextProps?.style, ] const $footerStyle = [ themed($footerPresets[preset]), (isHeadingPresent || isContentPresent) && { marginTop: spacing.xxxs }, $footerStyleOverride, FooterTextProps?.style, ] const $alignmentWrapperStyle = [ $alignmentWrapper, { justifyContent: $alignmentWrapperFlexOptions[verticalAlignment] }, LeftComponent && { marginStart: spacing.md }, RightComponent && { marginEnd: spacing.md }, ] return ( {LeftComponent} {HeadingComponent || (isHeadingPresent && ( ))} {ContentComponent || (isContentPresent && ( ))} {FooterComponent || (isFooterPresent && ( ))} {RightComponent} ) } const $containerBase: ThemedStyle = (theme) => ({ borderRadius: theme.spacing.md, padding: theme.spacing.xs, borderWidth: 1, shadowColor: theme.colors.palette.neutral800, shadowOffset: { width: 0, height: s(12) }, shadowOpacity: 0.08, shadowRadius: s(12.81), elevation: 16, minHeight: s(96), }) const $alignmentWrapper: ViewStyle = { flex: 1, alignSelf: "stretch", } const $alignmentWrapperFlexOptions = { "top": "flex-start", "center": "center", "space-between": "space-between", "force-footer-bottom": "space-between", } as const const $containerPresets: Record> = { default: [ $styles.row, $containerBase, (theme) => ({ backgroundColor: theme.colors.palette.neutral100, borderColor: theme.colors.palette.neutral300, }), ], reversed: [ $styles.row, $containerBase, (theme) => ({ backgroundColor: theme.colors.palette.neutral800, borderColor: theme.colors.palette.neutral500, }), ], } const $headingPresets: Record> = { default: [], reversed: [(theme) => ({ color: theme.colors.palette.neutral100 })], } const $contentPresets: Record> = { default: [], reversed: [(theme) => ({ color: theme.colors.palette.neutral100 })], } const $footerPresets: Record> = { default: [], reversed: [(theme) => ({ color: theme.colors.palette.neutral100 })], }