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/ui/atoms/avatar/index.tsx b/src/ui/atoms/avatar/index.tsx new file mode 100644 index 00000000..49777d08 --- /dev/null +++ b/src/ui/atoms/avatar/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import styled from 'styled-components'; +import { IconProfile } from 'static/icons'; + +import { useImageLoad } from './use-image-load'; + +const AvatarContainer = styled.div` + --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%; + } +`; + +interface AvatarProps { + alt?: string; + src?: string; + srcSet?: string; + children?: React.ReactNode; +} + +export const Avatar: React.FC = ({ alt, src, srcSet, children: childrenProp }) => { + 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}; +}; diff --git a/src/ui/atoms/avatar/usage.mdx b/src/ui/atoms/avatar/usage.mdx new file mode 100644 index 00000000..65f7380e --- /dev/null +++ b/src/ui/atoms/avatar/usage.mdx @@ -0,0 +1,73 @@ +--- +name: avatar +category: atoms +package: woly +--- + +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/ui/atoms/avatar/use-image-load.ts b/src/ui/atoms/avatar/use-image-load.ts new file mode 100644 index 00000000..fefbfe56 --- /dev/null +++ b/src/ui/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/ui/atoms/index.ts b/src/ui/atoms/index.ts index 7b484e4f..9f61c84a 100644 --- a/src/ui/atoms/index.ts +++ b/src/ui/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';