Skip to content

Commit

Permalink
feat: add images, icons loading skeletons and fallback items
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoVan committed Mar 23, 2024
1 parent f0606ab commit b720f38
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 8 deletions.
18 changes: 16 additions & 2 deletions src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { ArticleTextBlockComponent } from '../ArticleTextBlockComponent/ArticleT
import { Article, ArticleTextBlock } from '../../model/types/article';
import cls from './ArticleListItem.module.scss';
import { getRouterArticleDetails } from '@/shared/const/router';
import { AppImage } from '@/shared/ui/AppImage';
import { Skeleton } from '@/shared/ui/Skeleton';

interface ArticleListItemProps {
className?: string;
Expand Down Expand Up @@ -51,7 +53,13 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => {
</div>
<Text text={article.title} className={cls.title} />
{types}
<img src={article.img} className={cls.img} alt={article.title} />
<AppImage
src={article.img}
className={cls.img}
alt={article.title}
fallback={<Skeleton width="100%" height={250} />}
errorFallback={<Skeleton width="100%" height={250} />}
/>
{textBlock && (
<ArticleTextBlockComponent block={textBlock} className={cls.textBlock} />
)}
Expand Down Expand Up @@ -79,7 +87,13 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => {
>
<Card>
<div className={cls.imageWrapper}>
<img src={article.img} alt={article.title} className={cls.img} />
<AppImage
src={article.img}
alt={article.title}
className={cls.img}
fallback={<Skeleton width={200} height={200} />}
errorFallback={<Skeleton width={200} height={200} />}
/>
<Text text={article.createdAt} className={cls.date} />
</div>
<div className={cls.infoWrapper}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const AvatarDropDown = memo(() => {
onClick: onLogout,
},
]}
trigger={<Avatar size={30} src={authData.avatar} />}
trigger={<Avatar size={30} src={authData.avatar} fallbackInverted />}
/>
);
});
2 changes: 1 addition & 1 deletion src/shared/assets/icons/list-24-24.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/shared/assets/icons/user-filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/shared/ui/AppImage/AppImage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { AppImage } from './AppImage';

export default {
title: 'shared/AppImage',
component: AppImage,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof AppImage>;

const Template: ComponentStory<typeof AppImage> = (args) => <AppImage {...args} />;

export const Normal = Template.bind({});
Normal.args = {};
49 changes: 49 additions & 0 deletions src/shared/ui/AppImage/AppImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
ImgHTMLAttributes, memo, ReactElement, useLayoutEffect, useState,
} from 'react';

interface AppImageProps extends ImgHTMLAttributes<HTMLImageElement>{
className?: string;
fallback?: ReactElement;
errorFallback?: ReactElement;
}

export const AppImage = memo((props: AppImageProps) => {
const {
className,
fallback,
errorFallback,
src,
alt = 'image',
...otherProps
} = props;
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);

useLayoutEffect(() => {
const img = new Image();
img.src = src ?? '';
img.onload = () => {
setIsLoading(false);
};
img.onerror = () => {
setIsLoading(false);
setHasError(true);
};
}, [src]);

if (isLoading && fallback) {
return fallback;
}
if (hasError && errorFallback) {
return errorFallback;
}
return (
<img
src={src}
alt={alt}
className={className}
{...otherProps}
/>
);
});
1 change: 1 addition & 0 deletions src/shared/ui/AppImage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AppImage } from './AppImage';
19 changes: 15 additions & 4 deletions src/shared/ui/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
import { CSSProperties, useMemo } from 'react';
import { classNames, Mods } from '@/shared/lib/classNames/classNames';
import cls from './Avatar.module.scss';
import { AppImage } from '../AppImage';
import UserIcon from '../../assets/icons/user-filled.svg';
import { Icon } from '../Icon';
import { Skeleton } from '../Skeleton';

export interface AvatarProps {
className?: string;
src?: string;
size?: number;
alt?: string;
fallbackInverted?: boolean;
}

export const Avatar = (props: AvatarProps) => {
const {
className,
src,
size,
size = 100,
alt,
fallbackInverted,
} = props;
const mods: Mods = {};

const styles = useMemo<CSSProperties>(() => ({
width: size || 100,
height: size || 100,
width: size,
height: size,
}), [size]);

const fallback = <Skeleton width={size} height={size} border="50%" />;
const errorFallback = <Icon width={size} height={size} Svg={UserIcon} inverted={fallbackInverted} />;

return (
<img
<AppImage
src={src}
style={styles}
className={classNames(cls.Avatar, mods, [className])}
alt={alt}
fallback={fallback}
errorFallback={errorFallback}
/>
);
};
2 changes: 2 additions & 0 deletions src/shared/ui/Icon/Icon.module.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
.Icon {
fill: var(--primary-color);
color: var(--primary-color);
}

.inverted {
fill: var(--inverted-primary-color);
color: var(--inverted-primary-color);
}

0 comments on commit b720f38

Please sign in to comment.