diff options
Diffstat (limited to 'apps/mobile/components/ui')
| -rw-r--r-- | apps/mobile/components/ui/Avatar.tsx | 88 | ||||
| -rw-r--r-- | apps/mobile/components/ui/CustomSafeAreaView.tsx | 1 | ||||
| -rw-r--r-- | apps/mobile/components/ui/List.tsx | 41 |
3 files changed, 114 insertions, 16 deletions
diff --git a/apps/mobile/components/ui/Avatar.tsx b/apps/mobile/components/ui/Avatar.tsx new file mode 100644 index 00000000..923c634e --- /dev/null +++ b/apps/mobile/components/ui/Avatar.tsx @@ -0,0 +1,88 @@ +import * as React from "react"; +import { Image, View } from "react-native"; +import { Text } from "@/components/ui/Text"; +import { useAssetUrl } from "@/lib/hooks"; +import { cn } from "@/lib/utils"; + +interface AvatarProps { + image?: string | null; + name?: string | null; + size?: number; + className?: string; + imageClassName?: string; + fallbackClassName?: string; +} + +function isExternalUrl(url: string) { + return url.startsWith("http://") || url.startsWith("https://"); +} + +export function Avatar({ + image, + name, + size = 40, + className, + imageClassName, + fallbackClassName, +}: AvatarProps) { + const [imageError, setImageError] = React.useState(false); + const assetUrl = useAssetUrl(image ?? ""); + + const imageUrl = React.useMemo(() => { + if (!image) return null; + return isExternalUrl(image) + ? { + uri: image, + } + : assetUrl; + }, [image]); + + React.useEffect(() => { + setImageError(false); + }, [image]); + + const initials = React.useMemo(() => { + if (!name) return "U"; + return name.charAt(0).toUpperCase(); + }, [name]); + + const showFallback = !imageUrl || imageError; + + return ( + <View + className={cn("overflow-hidden bg-black", className)} + style={{ + width: size, + height: size, + borderRadius: size / 2, + }} + > + {showFallback ? ( + <View + className={cn( + "flex h-full w-full items-center justify-center bg-black", + fallbackClassName, + )} + > + <Text + className="text-white" + style={{ + fontSize: size * 0.4, + lineHeight: size * 0.4, + textAlign: "center", + }} + > + {initials} + </Text> + </View> + ) : ( + <Image + source={imageUrl} + className={cn("h-full w-full", imageClassName)} + style={{ resizeMode: "cover" }} + onError={() => setImageError(true)} + /> + )} + </View> + ); +} diff --git a/apps/mobile/components/ui/CustomSafeAreaView.tsx b/apps/mobile/components/ui/CustomSafeAreaView.tsx index fdf6520d..840ea058 100644 --- a/apps/mobile/components/ui/CustomSafeAreaView.tsx +++ b/apps/mobile/components/ui/CustomSafeAreaView.tsx @@ -15,6 +15,7 @@ export default function CustomSafeAreaView({ return ( <SafeAreaView style={{ + flex: 1, paddingTop: // Some ugly hacks to make the app look the same on both android and ios Platform.OS == "android" && edges.includes("top") diff --git a/apps/mobile/components/ui/List.tsx b/apps/mobile/components/ui/List.tsx index 52ff5779..67f0a9af 100644 --- a/apps/mobile/components/ui/List.tsx +++ b/apps/mobile/components/ui/List.tsx @@ -29,7 +29,9 @@ cssInterop(FlashList, { type ListDataItem = string | { title: string; subTitle?: string }; type ListVariant = "insets" | "full-width"; -type ListRef<T extends ListDataItem> = React.Ref<typeof FlashList<T>>; +type ListRef<T extends ListDataItem> = React.Ref< + React.ComponentRef<typeof FlashList<T>> +>; type ListRenderItemProps<T extends ListDataItem> = ListRenderItemInfo<T> & { variant?: ListVariant; @@ -76,17 +78,20 @@ const rootVariants = cva("min-h-2 flex-1", { }, }); -function ListComponent<T extends ListDataItem>({ - variant = "full-width", - rootClassName, - rootStyle, - contentContainerClassName, - renderItem, - data, - sectionHeaderAsGap = false, - contentInsetAdjustmentBehavior = "automatic", - ...props -}: ListProps<T>) { +function ListComponent<T extends ListDataItem>( + { + variant = "full-width", + rootClassName, + rootStyle, + contentContainerClassName, + renderItem, + data, + sectionHeaderAsGap = false, + contentInsetAdjustmentBehavior = "automatic", + ...props + }: ListProps<T>, + ref: ListRef<T>, +) { const insets = useSafeAreaInsets(); return ( <View @@ -100,6 +105,7 @@ function ListComponent<T extends ListDataItem>({ style={rootStyle} > <FlashList + ref={ref} data={data} contentInsetAdjustmentBehavior={contentInsetAdjustmentBehavior} renderItem={renderItemWithVariant( @@ -311,10 +317,9 @@ function ListItemComponent<T extends ListDataItem>( {!!leftView && <View>{leftView}</View>} <View className={cn( - "h-full flex-1 flex-row", + "h-full flex-1 flex-row pr-4", !item.subTitle ? "ios:py-3 py-[18px]" : "ios:py-2 py-2", !leftView && "ml-4", - !rightView && "pr-4", !removeSeparator && (!isLastInSection || variant === "full-width") && "ios:border-b ios:border-border/80", @@ -328,7 +333,7 @@ function ListItemComponent<T extends ListDataItem>( <Text numberOfLines={textNumberOfLines} style={titleStyle} - className={titleClassName} + className={cn("text-base", titleClassName)} > {item.title} </Text> @@ -343,7 +348,11 @@ function ListItemComponent<T extends ListDataItem>( </Text> )} </View> - {!!rightView && <View>{rightView}</View>} + {!!rightView && ( + <View className="flex items-center justify-center"> + {rightView} + </View> + )} </View> </TextClassContext.Provider> </Button> |
