Pārlūkot izejas kodu

Feature: Avatar component completed.

lfabl 1 dienu atpakaļ
vecāks
revīzija
4d3fc8ce6c

+ 5 - 1
example/src/pages/home/index.tsx

@@ -61,7 +61,11 @@ const Home = () => {
         <PaletteSwitcher/>
         <ThemeSwitcher/>
         <Avatar
-            title="MA"
+            title="Furkan Atakan BOZKURT"
+            isStatusIndicator={true}
+            isWorkWithAction={true}
+            size="large"
+            // imageUrl="https://fotolifeakademi.com/uploads/2020/04/manzara-fotografi-cekmek-724x394.webp"
         />
         <View
             style={{

+ 4 - 3
src/assets/svg/loadingIcon/index.tsx

@@ -4,6 +4,7 @@ import {
 } from "react";
 import {
     Animated,
+    Platform,
     Easing,
     View
 } from "react-native";
@@ -18,7 +19,7 @@ import {
     Svg
 } from "react-native-svg";
 
-const SvgLoadingIcon = ({
+const LoadingIcon = ({
     color = "emphasized",
     customColor,
     size = 22,
@@ -38,7 +39,7 @@ const SvgLoadingIcon = ({
                 toValue: 1,
                 duration: 2000,
                 easing: Easing.linear,
-                useNativeDriver: true
+                useNativeDriver: Platform.OS !== "web"
             })
         );
 
@@ -92,4 +93,4 @@ const SvgLoadingIcon = ({
         </Animated.View>
     </View>;
 };
-export default SvgLoadingIcon;
+export default LoadingIcon;

+ 143 - 13
src/components/avatar/index.tsx

@@ -1,6 +1,10 @@
+import {
+    useState
+} from "react";
 import {
     TouchableOpacity,
-    Image
+    Image,
+    View
 } from "react-native";
 import type IAvatarProps from "./type";
 import stylesheet, {
@@ -13,18 +17,23 @@ import {
 import {
     UserRound as UserRoundIcon
 } from "lucide-react-native";
+import Loading from "../loading";
 import Text from "../text";
 
 const Avatar = ({
-    backgroundColor = "primary",
+    titleAbbreviationType = "first-last",
+    isShowStatusIndicatorSplicer = true,
+    avatarBackgroundColor = "default",
+    statusIndicatorType = "success",
     borderColor = "emphasized",
     titleColor = "onPrimary",
-    statusIndicatorType,
     isStatusIndicator,
     isWorkWithAction,
     image: ImageProp,
+    backgroundColor,
     size = "medium",
     icon: IconProp,
+    imageSource,
     customTheme,
     isDisabled,
     imageProps,
@@ -36,7 +45,7 @@ const Avatar = ({
     style
 }: IAvatarProps) => {
     const {
-        radiuses,
+        borders,
         colors,
         spaces
     } = NCoreUIKitTheme.useContext(customTheme);
@@ -46,19 +55,69 @@ const Avatar = ({
         size
     });
 
+    const [
+        imageLoadError,
+        setImageLoadError
+    ] = useState(false);
+
     const {
-        container: containerDynamicStyle
+        statusIndicator: statusIndicatorDynamicStyle,
+        container: containerDynamicStyle,
+        image: imageDynamicStyle,
+        icon: iconDynamicStyle
     } = useStyles({
+        isShowStatusIndicatorSplicer,
+        avatarBackgroundColor,
+        statusIndicatorType,
+        image: ImageProp,
         backgroundColor,
         borderColor,
         currentSize,
-        iconColor,
-        radiuses,
+        isDisabled,
+        isLoading,
+        imageUrl,
+        borders,
         colors,
-        spaces
+        title
     });
 
     const calculateShortTitle = () => {
+        if(!title) {
+            return "";
+        }
+
+        const allWords: Array<string> = title.split(" ");
+
+        if(titleAbbreviationType === "every-first") {
+            let newString = "";
+
+            allWords.forEach((wordItem, wordIndex) => {
+                if(wordIndex < 3) newString += wordItem[0];
+            });
+
+            return newString;
+        }
+
+        if(titleAbbreviationType === "first-last") {
+            let newString = "";
+
+            if(allWords[0]) newString += allWords[0][0];
+
+            if(allWords.length > 1 && allWords[allWords.length - 1]) newString += allWords[allWords.length - 1]?.[0];
+
+            return newString;
+        }
+
+        if(titleAbbreviationType === "first-next") {
+            let newString = "";
+
+            if(allWords[0]) newString += allWords[0][0];
+
+            if(allWords[1]) newString += allWords[1][0];
+
+            return newString;
+        }
+
         return title;
     };
 
@@ -67,17 +126,54 @@ const Avatar = ({
             return <IconProp
                 size={currentSize.iconSize}
                 customColor={iconColor}
-                color="danger"
+                color="default"
+                style={{
+                    ...iconDynamicStyle
+                }}
             />;
         }
 
         return <UserRoundIcon
+            color={iconColor ? iconColor : colors.content.icon.default}
             size={currentSize.iconSize}
-            color={iconColor}
+            style={{
+                ...iconDynamicStyle
+            }}
+        />;
+    };
+
+    const renderStatusIndicator = () => {
+        if(!isStatusIndicator) {
+            return null;
+        }
+
+        return <View
+            style={[
+                stylesheet.statusIndicator,
+                statusIndicatorDynamicStyle
+            ]}
+        />;
+    };
+
+    const renderLoading = () => {
+        let loadingColor: keyof NCoreUIKit.IconContentColors = "default";
+
+        if(title) {
+            loadingColor = "onPrimary";
+        } else if(imageUrl || ImageProp) {
+            loadingColor = "mid";
+        }
+
+        return <Loading
+            color={loadingColor}
         />;
     };
 
     const renderContent = () => {
+        if(isLoading) {
+            return renderLoading();
+        }
+
         if(IconProp) {
             return renderIcon();
         }
@@ -86,15 +182,48 @@ const Avatar = ({
             return renderIcon();
         }
 
-        if(ImageProp) {
-            const TargetImage = ImageProp();
+        if((imageUrl || ImageProp) && imageLoadError) {
+            if(title) {
+                return <Text
+                    variant={currentSize.fontSize}
+                    color={titleColor}
+                >
+                    {calculateShortTitle()}
+                </Text>;
+            } else {
+                return renderIcon();
+            }
+        }
 
-            return <TargetImage/>;
+        if(ImageProp) {
+            return <ImageProp
+                {...imageProps}
+                onError={() => setImageLoadError(true)}
+                height={currentSize.size}
+                width={currentSize.size}
+                style={[
+                    stylesheet.image,
+                    imageDynamicStyle,
+                    imageProps?.style
+                ]}
+            />;
         }
 
         if(imageUrl) {
             return <Image
                 {...imageProps}
+                onError={() => setImageLoadError(true)}
+                height={currentSize.size}
+                width={currentSize.size}
+                source={{
+                    uri: imageUrl,
+                    ...imageSource
+                }}
+                style={[
+                    stylesheet.image,
+                    imageDynamicStyle,
+                    imageProps?.style
+                ]}
             />;
         }
 
@@ -118,6 +247,7 @@ const Avatar = ({
         ]}
     >
         {renderContent()}
+        {renderStatusIndicator()}
     </TouchableOpacity>;
 };
 export default Avatar;

+ 102 - 31
src/components/avatar/stylesheet.ts

@@ -1,4 +1,5 @@
 import {
+    type ImageStyle,
     type ViewStyle,
     StyleSheet
 } from "react-native";
@@ -14,80 +15,150 @@ import type {
 } from "../../types";
 
 export const AVATAR_SIZES: Record<AvatarSizeType, AvatarMeasuresKeys> = {
-    xSmall: {
-        paddingHorizontal: "spacingXs",
-        paddingVertical: "spacingXs",
+    xxSmall: {
         fontSize: "labelSmallSize",
-        iconSize: 14
+        statusIndicatorSize: 8,
+        iconSize: 14,
+        size: 24
     },
-    small: {
-        paddingHorizontal: "spacingSm",
-        paddingVertical: "spacingSm",
+    xSmall: {
         fontSize: "labelMediumSize",
-        iconSize: 18
+        statusIndicatorSize: 10,
+        iconSize: 18,
+        size: 32
     },
-    medium: {
-        paddingHorizontal: "spacingMd",
-        paddingVertical: "spacingMd",
+    small: {
         fontSize: "labelLargeSize",
-        iconSize: 22
+        statusIndicatorSize: 12,
+        iconSize: 22,
+        size: 40
     },
-    large: {
-        paddingHorizontal: "spacingLg",
-        paddingVertical: "spacingLg",
+    medium: {
         fontSize: "bodyLargeSize",
-        iconSize: 26
+        statusIndicatorSize: 14,
+        iconSize: 26,
+        size: 48
     },
-    xLarge: {
-        paddingHorizontal: "spacingXl",
+    large: {
         fontSize: "headlineSmallSize",
-        paddingVertical: "spacingXl",
-        iconSize: 32
+        statusIndicatorSize: 16,
+        iconSize: 32,
+        size: 64
+    },
+    xLarge: {
+        fontSize: "headlineMediumSize",
+        statusIndicatorSize: 20,
+        iconSize: 40,
+        size: 80
+    },
+    xxLarge: {
+        fontSize: "displaySmallSize",
+        statusIndicatorSize: 28,
+        iconSize: 60,
+        size: 120
+    },
+    xxxLarge: {
+        fontSize: "displayMediumSize",
+        statusIndicatorSize: 32,
+        iconSize: 76,
+        size: 150
     }
 };
 
 export const getAvatarSize = ({
-    spaces,
     size
 }: AvatarSizeConstantType): AvatarMeasures => {
     const currentSize = AVATAR_SIZES[size];
 
     return {
-        paddingHorizontal: spaces[currentSize.paddingHorizontal],
-        paddingVertical: spaces[currentSize.paddingVertical],
+        statusIndicatorSize: currentSize.statusIndicatorSize,
         fontSize: currentSize.fontSize,
-        iconSize: currentSize.iconSize
+        iconSize: currentSize.iconSize,
+        size: currentSize.size
     };
 };
 
 const stylesheet = StyleSheet.create({
     container: {
-        overflow: "hidden"
+        position: "relative",
+        display: "flex"
+    },
+    image: {
+        objectFit: "cover"
+    },
+    icon: {
+    },
+    statusIndicator: {
+        position: "absolute",
+        bottom: 0,
+        right: 0
     }
 });
 
 export const useStyles = ({
+    isShowStatusIndicatorSplicer,
+    avatarBackgroundColor,
+    statusIndicatorType,
     backgroundColor,
     borderColor,
     currentSize,
-    iconColor,
-    radiuses,
+    isDisabled,
+    isLoading,
     imageUrl,
+    borders,
     colors,
-    spaces,
     image,
     title
 }: AvatarDynamicStyleType) => {
     const styles = {
         container: {
-            backgroundColor: backgroundColor === "transparent" ? "transparent" : colors.content.container[backgroundColor],
-            borderRadius: radiuses.full,
-            padding: spaces.spacingMd
+            backgroundColor: backgroundColor ? backgroundColor === "transparent" ? "transparent" : colors.content.container[backgroundColor] : colors.content.container.subtle,
+            borderColor: borderColor ? borderColor : "transparent",
+            borderRadius: currentSize.size / 2,
+            height: currentSize.size,
+            width: currentSize.size
+        } as Mutable<ViewStyle>,
+        image: {
+            height: currentSize.size,
+            width: currentSize.size
+        } as Mutable<ImageStyle>,
+        icon: {
+        } as Mutable<ViewStyle>,
+        statusIndicator: {
+            borderColor: colors.content.container[avatarBackgroundColor],
+            backgroundColor: colors.content.icon[statusIndicatorType],
+            borderRadius: currentSize.statusIndicatorSize / 2,
+            height: currentSize.statusIndicatorSize,
+            width: currentSize.statusIndicatorSize
         } as Mutable<ViewStyle>
     };
 
     if(!title && !image && !imageUrl && !backgroundColor) {
         styles.container.backgroundColor = colors.content.container.subtle;
+
+        styles.container.justifyContent = "center";
+        styles.container.alignItems = "center";
+    }
+
+    if(title) {
+        styles.container.backgroundColor = colors.content.container.primary;
+
+        styles.container.justifyContent = "center";
+        styles.container.alignItems = "center";
+    }
+
+    if(isShowStatusIndicatorSplicer) {
+        const multiplier = currentSize.statusIndicatorSize / 14;
+
+        styles.statusIndicator.borderWidth = borders.subtract * multiplier;
+    }
+
+    if(isDisabled) {
+        styles.container.opacity = 0.33;
+    }
+
+    if(isLoading) {
+        styles.container.opacity = 0.33;
     }
 
     return styles;

+ 26 - 14
src/components/avatar/type.ts

@@ -1,38 +1,44 @@
+import type {
+    ComponentType
+} from "react";
 import {
+    type ImageURISource,
     type ImageProps,
     type TextStyle,
-    type StyleProp,
-    Image
+    type StyleProp
 } from "react-native";
 import type {
     NCoreUIKitIcon
 } from "../../types";
 
 export type AvatarDynamicStyleType = {
-    backgroundColor: keyof NCoreUIKit.ContainerContentColors | "transparent";
+    backgroundColor?: keyof NCoreUIKit.ContainerContentColors | "transparent";
     borderColor: keyof NCoreUIKit.BorderContentColors | "transparent";
-    iconColor: keyof NCoreUIKit.ProjectColorPalette;
-    radiuses: NCoreUIKit.ActivePalette["radiuses"];
-    spaces: NCoreUIKit.ActivePalette["spaces"];
+    avatarBackgroundColor: keyof NCoreUIKit.ContainerContentColors;
+    statusIndicatorType: AvatarStatusIndıcatorType;
+    borders: NCoreUIKit.ActivePalette["borders"];
     colors: NCoreUIKit.ActivePalette["colors"];
+    isShowStatusIndicatorSplicer?: boolean;
+    image?: ComponentType<ImageProps>;
     currentSize: AvatarMeasures;
-    image?: () => typeof Image;
+    isDisabled?: boolean;
+    isLoading?: boolean;
     imageUrl?: string;
     title?: string;
 };
 
 export type AvatarMeasuresKeys = {
-    paddingHorizontal: keyof NCoreUIKit.Spaces;
-    paddingVertical: keyof NCoreUIKit.Spaces;
     fontSize: keyof NCoreUIKit.Typography;
+    statusIndicatorSize: number;
     iconSize: number;
+    size: number;
 };
 
 export type AvatarMeasures = {
     fontSize: keyof NCoreUIKit.Typography;
-    paddingHorizontal: number;
-    paddingVertical: number;
+    statusIndicatorSize: number;
     iconSize: number;
+    size: number;
 };
 
 export type AvatarSizeConstantType = {
@@ -40,13 +46,16 @@ export type AvatarSizeConstantType = {
     size: AvatarSizeType;
 };
 
-export type AvatarStatusIndıcatorType = keyof NCoreUIKit.ContainerContentColors;
+export type AvatarStatusIndıcatorType = keyof NCoreUIKit.IconContentColors;
+
+export type AvatarTitleAbbreviationTypes = "first-last" | "first-next" | "every-first";
 
-export type AvatarSizeType = "xSmall" | "small" | "medium" | "large" | "xLarge";
+export type AvatarSizeType = "xxSmall" | "xSmall" | "small" | "medium" | "large" | "xLarge" | "xxLarge" | "xxxLarge";
 
 interface IAvatarProps {
     backgroundColor?: keyof NCoreUIKit.ContainerContentColors | "transparent";
     borderColor?: keyof NCoreUIKit.BorderContentColors | "transparent";
+    avatarBackgroundColor?: keyof NCoreUIKit.ContainerContentColors;
     iconColor?: keyof NCoreUIKit.ProjectColorPalette;
     customTheme?: {
         gapPropagation?: keyof NCoreUIKit.GapPropagationKey;
@@ -55,11 +64,14 @@ interface IAvatarProps {
         themeKey?: keyof NCoreUIKit.ThemeKey;
     };
     style?: StyleProp<TextStyle>[] | StyleProp<TextStyle>;
+    titleAbbreviationType?: AvatarTitleAbbreviationTypes;
     statusIndicatorType?: AvatarStatusIndıcatorType;
     titleColor?: keyof NCoreUIKit.TextContentColors;
+    isShowStatusIndicatorSplicer?: boolean;
+    image?: ComponentType<ImageProps>;
+    imageSource?: ImageURISource;
     isStatusIndicator?: boolean;
     isWorkWithAction?: boolean;
-    image?: () => typeof Image;
     imageProps?: ImageProps;
     icon?: NCoreUIKitIcon;
     size?: AvatarSizeType;

+ 4 - 4
src/components/loading/index.tsx

@@ -17,10 +17,10 @@ const Loading: FC<ILoadingProps> = ({
         customColor={customColor}
         color={color}
         size={size}
-        style={[
-            stylesheet.container,
-            style
-        ]}
+        style={{
+            ...stylesheet.container,
+            ...style
+        }}
     />;
 };
 export default Loading;