316 lines
8.7 KiB
TypeScript
316 lines
8.7 KiB
TypeScript
|
|
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<TextStyle>
|
||
|
|
/**
|
||
|
|
* 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<TextStyle>
|
||
|
|
/**
|
||
|
|
* 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<TextStyle>
|
||
|
|
/**
|
||
|
|
* 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<ViewStyle> = [
|
||
|
|
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 (
|
||
|
|
<Wrapper
|
||
|
|
style={$containerStyle}
|
||
|
|
activeOpacity={0.8}
|
||
|
|
accessibilityRole={isPressable ? "button" : undefined}
|
||
|
|
{...WrapperProps}
|
||
|
|
>
|
||
|
|
{LeftComponent}
|
||
|
|
|
||
|
|
<View style={$alignmentWrapperStyle}>
|
||
|
|
<HeaderContentWrapper>
|
||
|
|
{HeadingComponent ||
|
||
|
|
(isHeadingPresent && (
|
||
|
|
<Text
|
||
|
|
weight="bold"
|
||
|
|
text={heading}
|
||
|
|
tx={headingTx}
|
||
|
|
txOptions={headingTxOptions}
|
||
|
|
{...HeadingTextProps}
|
||
|
|
style={$headingStyle}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
|
||
|
|
{ContentComponent ||
|
||
|
|
(isContentPresent && (
|
||
|
|
<Text
|
||
|
|
weight="normal"
|
||
|
|
text={content}
|
||
|
|
tx={contentTx}
|
||
|
|
txOptions={contentTxOptions}
|
||
|
|
{...ContentTextProps}
|
||
|
|
style={$contentStyle}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</HeaderContentWrapper>
|
||
|
|
|
||
|
|
{FooterComponent ||
|
||
|
|
(isFooterPresent && (
|
||
|
|
<Text
|
||
|
|
weight="normal"
|
||
|
|
size="xs"
|
||
|
|
text={footer}
|
||
|
|
tx={footerTx}
|
||
|
|
txOptions={footerTxOptions}
|
||
|
|
{...FooterTextProps}
|
||
|
|
style={$footerStyle}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</View>
|
||
|
|
|
||
|
|
{RightComponent}
|
||
|
|
</Wrapper>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const $containerBase: ThemedStyle<ViewStyle> = (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<Presets, ThemedStyleArray<ViewStyle>> = {
|
||
|
|
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<Presets, ThemedStyleArray<TextStyle>> = {
|
||
|
|
default: [],
|
||
|
|
reversed: [(theme) => ({ color: theme.colors.palette.neutral100 })],
|
||
|
|
}
|
||
|
|
|
||
|
|
const $contentPresets: Record<Presets, ThemedStyleArray<TextStyle>> = {
|
||
|
|
default: [],
|
||
|
|
reversed: [(theme) => ({ color: theme.colors.palette.neutral100 })],
|
||
|
|
}
|
||
|
|
|
||
|
|
const $footerPresets: Record<Presets, ThemedStyleArray<TextStyle>> = {
|
||
|
|
default: [],
|
||
|
|
reversed: [(theme) => ({ color: theme.colors.palette.neutral100 })],
|
||
|
|
}
|