diff --git a/src/static/icons/index.ts b/src/static/icons/index.ts index 5f1ac341..65413845 100644 --- a/src/static/icons/index.ts +++ b/src/static/icons/index.ts @@ -11,3 +11,4 @@ export { default as IconPlus } from './plus.svg'; export { default as IconSearch } from './search.svg'; export { default as IconFilledUnchecked } from './check-filled-unchecked.svg'; export { default as IconSpinner } from './spinner.svg'; +export { default as IconProfile } from './profile.svg'; diff --git a/src/static/icons/profile.svg b/src/static/icons/profile.svg new file mode 100644 index 00000000..484b6645 --- /dev/null +++ b/src/static/icons/profile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/woly/atoms/avatar/index.tsx b/src/woly/atoms/avatar/index.tsx new file mode 100644 index 00000000..5735e3a8 --- /dev/null +++ b/src/woly/atoms/avatar/index.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import styled, { StyledComponent } from 'styled-components'; +import { IconProfile } from 'static/icons'; + +import { useImageLoad } from './use-image-load'; + +interface AvatarProps { + alt?: string; + src?: string; + srcSet?: string; + children?: React.ReactNode; +} + +const AvatarBase: React.FC = ({ + alt, + children: childrenProp, + src, + srcSet, + ...props +}) => { + const loadFailed = useImageLoad({ src, srcSet }); + const hasImg = src || srcSet; + let children = null; + + if (hasImg && !loadFailed) { + children = {alt}; + } else if (childrenProp) { + children = childrenProp; + } else { + // render fallback if image loading failed or no src attributes / children provided + children = ; + } + + return
{children}
; +}; + +export const Avatar = styled(AvatarBase)` + --local-size: calc((var(--woly-component-level) + 2) * 2 * var(--woly-const-m)); + + width: var(--local-size); + height: var(--local-size); + + & > * { + width: 100%; + height: 100%; + + border-radius: 50%; + } +` as StyledComponent<'div', Record, AvatarProps>; diff --git a/src/woly/atoms/avatar/usage.mdx b/src/woly/atoms/avatar/usage.mdx new file mode 100644 index 00000000..b5c08468 --- /dev/null +++ b/src/woly/atoms/avatar/usage.mdx @@ -0,0 +1,67 @@ +import {Avatar} from 'ui' +import {Playground, block} from 'lib/playground' + +`Avatar` shows user avatar + +### Example + + + + + +### Sizes + + + + + + + + + + + + + + + + + + + + + + +### Fallback + + + + + +### Custom child component + + + +
+ RC +
+
+
+ +### Props + +| Name | Type | Default | Description | +| ---------- | ----------------- | ----------- | ---------------------------------------- | +| `alt` | `string` | `''` | text description of the image | +| `children` | `React.ReactNode` | `undefined` | use if no src attributes provided | +| `src` | `string` | `''` | avatar src | +| `srcSet` | `string` | `''` | avatar src set for multiple screen sizes | diff --git a/src/woly/atoms/avatar/use-image-load.ts b/src/woly/atoms/avatar/use-image-load.ts new file mode 100644 index 00000000..fefbfe56 --- /dev/null +++ b/src/woly/atoms/avatar/use-image-load.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; + +export function useImageLoad({ src, srcSet }: { src?: string; srcSet?: string }) { + const [failed, setFailed] = useState(false); + + useEffect(() => { + if (!src && !srcSet) { + return; + } + + setFailed(false); + + const image = new Image(); + image.src = src ?? ''; + image.srcset = srcSet ?? ''; + + const onLoad = () => { + setFailed(false); + }; + + const onError = () => { + setFailed(true); + }; + + image.addEventListener('load', onLoad); + image.addEventListener('error', onError); + + return () => { + image.removeEventListener('load', onLoad); + image.removeEventListener('error', onError); + }; + }, [src, srcSet]); + + return failed; +} diff --git a/src/woly/atoms/index.ts b/src/woly/atoms/index.ts index 7b484e4f..9f61c84a 100644 --- a/src/woly/atoms/index.ts +++ b/src/woly/atoms/index.ts @@ -16,3 +16,4 @@ export { Text } from './text'; export { TextArea } from './text-area'; export { Tooltip } from './tooltip'; export { UploadArea } from './upload-area'; +export { Avatar } from './avatar';