Skip to content

Commit

Permalink
Merge pull request #438 from commercelayer/avatar-placeholder
Browse files Browse the repository at this point in the history
Add an image placeholder to the `Avatar` component when `src` prop is not defined
  • Loading branch information
marcomontalbano authored Nov 22, 2023
2 parents 5a31de4 + 487bd6f commit 8afe11c
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 24 deletions.
13 changes: 13 additions & 0 deletions packages/app-elements/src/ui/atoms/Avatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,17 @@ describe('Avatar', () => {
expect(element.getAttribute('src')).toContain('data:image/')
expect(element.getAttribute('alt')).toBe('Stripe')
})

test('Should be rendered with a placeholder image when src is not defined', () => {
const { element } = setup({
id: 'avatar',
// @ts-expect-error I want to test this scenario.
src: undefined,
alt: 'Undefined source'
})
expect(element).toBeVisible()
expect(element).toMatchSnapshot()
expect(element.getAttribute('src')).toContain('data:image/')
expect(element.getAttribute('alt')).toBe('Undefined source')
})
})
41 changes: 32 additions & 9 deletions packages/app-elements/src/ui/atoms/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import cn from 'classnames'
import { presets } from './Avatar.utils'

type SrcPreset = keyof typeof presets
type SrcUrl = `https://${string}` | `data:image/${string}`

export interface AvatarProps {
/**
* Image URL
*/
src: keyof typeof presets | `https://${string}` | `data:image/${string}`
src: SrcPreset | SrcUrl
/**
* Alt text
*/
Expand All @@ -31,12 +34,6 @@ export interface AvatarProps {
className?: string
}

function srcIsValidPreset(
src: AvatarProps['src']
): src is keyof typeof presets {
return Object.keys(presets).includes(src)
}

/**
* This component renders as `<img>` using different shapes and sizes. It is mostly used to show an SKU image or a 3rd-party icon.
*/
Expand All @@ -52,7 +49,13 @@ function Avatar({
return (
<img
{...rest}
src={srcIsValidPreset(src) ? presets[src] : src}
src={
srcIsValidPreset(src)
? presets[src]
: srcIsValidUrl(src)
? src
: placeholderSvg
}
alt={alt}
className={cn(
'border object-contain object-center',
Expand All @@ -66,13 +69,33 @@ function Avatar({
'rounded-full': shape === 'circle',
// border
'border-gray-100': border == null,
'border-transparent': border === 'none'
'border-transparent': border === 'none',
// placeholder
'p-1':
!srcIsValidPreset(src) && !srcIsValidUrl(src) && size === 'normal',
'p-0.5':
!srcIsValidPreset(src) && !srcIsValidUrl(src) && size !== 'normal'
},
className
)}
/>
)
}

function srcIsValidPreset(src: AvatarProps['src']): src is SrcPreset {
return Object.keys(presets).includes(src)
}

function srcIsValidUrl(
src: AvatarProps['src'] | undefined | null
): src is SrcUrl {
return (
src != null && (src.startsWith('https://') || src.startsWith('data:image/'))
)
}

const placeholderSvg =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEwOF8xNSkiPgo8cmVjdCB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIGZpbGw9IiNFREVFRUUiLz4KPHBhdGggZD0iTTAgMEw0OCA0OCIgc3Ryb2tlPSIjRDFENURCIiBzdHJva2Utb3BhY2l0eT0iMC40IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPHBhdGggZD0iTTQ4IDBMMCA0OCIgc3Ryb2tlPSIjRDFENURCIiBzdHJva2Utb3BhY2l0eT0iMC40IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPHBhdGggZD0iTTMyIDE1SDE2QzE1LjQ0NzcgMTUgMTUgMTUuNDQ3NyAxNSAxNlYzMkMxNSAzMi41NTIzIDE1LjQ0NzcgMzMgMTYgMzNIMzJDMzIuNTUyMyAzMyAzMyAzMi41NTIzIDMzIDMyVjE2QzMzIDE1LjQ0NzcgMzIuNTUyMyAxNSAzMiAxNVoiIGZpbGw9IiNFREVFRUUiLz4KPHBhdGggZD0iTTI5LjYyNSAxNy4yNUgxOC4zNzVDMTguMDc2NiAxNy4yNSAxNy43OTA1IDE3LjM2ODUgMTcuNTc5NSAxNy41Nzk1QzE3LjM2ODUgMTcuNzkwNSAxNy4yNSAxOC4wNzY2IDE3LjI1IDE4LjM3NVYyOS42MjVDMTcuMjUgMjkuOTIzNCAxNy4zNjg1IDMwLjIwOTUgMTcuNTc5NSAzMC40MjA1QzE3Ljc5MDUgMzAuNjMxNSAxOC4wNzY2IDMwLjc1IDE4LjM3NSAzMC43NUgyOS42MjVDMjkuOTIzNCAzMC43NSAzMC4yMDk1IDMwLjYzMTUgMzAuNDIwNSAzMC40MjA1QzMwLjYzMTUgMzAuMjA5NSAzMC43NSAyOS45MjM0IDMwLjc1IDI5LjYyNVYxOC4zNzVDMzAuNzUgMTguMDc2NiAzMC42MzE1IDE3Ljc5MDUgMzAuNDIwNSAxNy41Nzk1QzMwLjIwOTUgMTcuMzY4NSAyOS45MjM0IDE3LjI1IDI5LjYyNSAxNy4yNVpNMTguMzc1IDE4LjM3NUgyOS42MjVWMjMuODE1OEwyNy44ODkgMjIuMDc5MUMyNy42NzggMjEuODY4MiAyNy4zOTIgMjEuNzQ5OCAyNy4wOTM4IDIxLjc0OThDMjYuNzk1NSAyMS43NDk4IDI2LjUwOTUgMjEuODY4MiAyNi4yOTg1IDIyLjA3OTFMMTguNzUyNiAyOS42MjVIMTguMzc1VjE4LjM3NVpNMjAuNjI1IDIxLjc1QzIwLjYyNSAyMS41Mjc1IDIwLjY5MSAyMS4zMSAyMC44MTQ2IDIxLjEyNUMyMC45MzgyIDIwLjk0IDIxLjExMzkgMjAuNzk1OCAyMS4zMTk1IDIwLjcxMDZDMjEuNTI1IDIwLjYyNTUgMjEuNzUxMiAyMC42MDMyIDIxLjk2OTUgMjAuNjQ2NkMyMi4xODc3IDIwLjY5IDIyLjM4ODIgMjAuNzk3MiAyMi41NDU1IDIwLjk1NDVDMjIuNzAyOCAyMS4xMTE4IDIyLjgxIDIxLjMxMjMgMjIuODUzNCAyMS41MzA1QzIyLjg5NjggMjEuNzQ4OCAyMi44NzQ1IDIxLjk3NSAyMi43ODk0IDIyLjE4MDVDMjIuNzA0MiAyMi4zODYxIDIyLjU2IDIyLjU2MTggMjIuMzc1IDIyLjY4NTRDMjIuMTkgMjIuODA5IDIxLjk3MjUgMjIuODc1IDIxLjc1IDIyLjg3NUMyMS40NTE2IDIyLjg3NSAyMS4xNjU1IDIyLjc1NjUgMjAuOTU0NSAyMi41NDU1QzIwLjc0MzUgMjIuMzM0NSAyMC42MjUgMjIuMDQ4NCAyMC42MjUgMjEuNzVaIiBmaWxsPSIjQkJCRUJFIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTA4XzE1Ij4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iMyIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K'

Avatar.displayName = 'Avatar'
export { Avatar }
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Avatar > Should be rendered with a placeholder image when src is not defined 1`] = `
<img
alt="Undefined source"
class="border object-contain object-center min-w-[58px] min-h-[58px] w-[58px] h-[58px] rounded border-gray-100 p-1"
data-testid="avatar"
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEwOF8xNSkiPgo8cmVjdCB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIGZpbGw9IiNFREVFRUUiLz4KPHBhdGggZD0iTTAgMEw0OCA0OCIgc3Ryb2tlPSIjRDFENURCIiBzdHJva2Utb3BhY2l0eT0iMC40IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPHBhdGggZD0iTTQ4IDBMMCA0OCIgc3Ryb2tlPSIjRDFENURCIiBzdHJva2Utb3BhY2l0eT0iMC40IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPHBhdGggZD0iTTMyIDE1SDE2QzE1LjQ0NzcgMTUgMTUgMTUuNDQ3NyAxNSAxNlYzMkMxNSAzMi41NTIzIDE1LjQ0NzcgMzMgMTYgMzNIMzJDMzIuNTUyMyAzMyAzMyAzMi41NTIzIDMzIDMyVjE2QzMzIDE1LjQ0NzcgMzIuNTUyMyAxNSAzMiAxNVoiIGZpbGw9IiNFREVFRUUiLz4KPHBhdGggZD0iTTI5LjYyNSAxNy4yNUgxOC4zNzVDMTguMDc2NiAxNy4yNSAxNy43OTA1IDE3LjM2ODUgMTcuNTc5NSAxNy41Nzk1QzE3LjM2ODUgMTcuNzkwNSAxNy4yNSAxOC4wNzY2IDE3LjI1IDE4LjM3NVYyOS42MjVDMTcuMjUgMjkuOTIzNCAxNy4zNjg1IDMwLjIwOTUgMTcuNTc5NSAzMC40MjA1QzE3Ljc5MDUgMzAuNjMxNSAxOC4wNzY2IDMwLjc1IDE4LjM3NSAzMC43NUgyOS42MjVDMjkuOTIzNCAzMC43NSAzMC4yMDk1IDMwLjYzMTUgMzAuNDIwNSAzMC40MjA1QzMwLjYzMTUgMzAuMjA5NSAzMC43NSAyOS45MjM0IDMwLjc1IDI5LjYyNVYxOC4zNzVDMzAuNzUgMTguMDc2NiAzMC42MzE1IDE3Ljc5MDUgMzAuNDIwNSAxNy41Nzk1QzMwLjIwOTUgMTcuMzY4NSAyOS45MjM0IDE3LjI1IDI5LjYyNSAxNy4yNVpNMTguMzc1IDE4LjM3NUgyOS42MjVWMjMuODE1OEwyNy44ODkgMjIuMDc5MUMyNy42NzggMjEuODY4MiAyNy4zOTIgMjEuNzQ5OCAyNy4wOTM4IDIxLjc0OThDMjYuNzk1NSAyMS43NDk4IDI2LjUwOTUgMjEuODY4MiAyNi4yOTg1IDIyLjA3OTFMMTguNzUyNiAyOS42MjVIMTguMzc1VjE4LjM3NVpNMjAuNjI1IDIxLjc1QzIwLjYyNSAyMS41Mjc1IDIwLjY5MSAyMS4zMSAyMC44MTQ2IDIxLjEyNUMyMC45MzgyIDIwLjk0IDIxLjExMzkgMjAuNzk1OCAyMS4zMTk1IDIwLjcxMDZDMjEuNTI1IDIwLjYyNTUgMjEuNzUxMiAyMC42MDMyIDIxLjk2OTUgMjAuNjQ2NkMyMi4xODc3IDIwLjY5IDIyLjM4ODIgMjAuNzk3MiAyMi41NDU1IDIwLjk1NDVDMjIuNzAyOCAyMS4xMTE4IDIyLjgxIDIxLjMxMjMgMjIuODUzNCAyMS41MzA1QzIyLjg5NjggMjEuNzQ4OCAyMi44NzQ1IDIxLjk3NSAyMi43ODk0IDIyLjE4MDVDMjIuNzA0MiAyMi4zODYxIDIyLjU2IDIyLjU2MTggMjIuMzc1IDIyLjY4NTRDMjIuMTkgMjIuODA5IDIxLjk3MjUgMjIuODc1IDIxLjc1IDIyLjg3NUMyMS40NTE2IDIyLjg3NSAyMS4xNjU1IDIyLjc1NjUgMjAuOTU0NSAyMi41NDU1QzIwLjc0MzUgMjIuMzM0NSAyMC42MjUgMjIuMDQ4NCAyMC42MjUgMjEuNzVaIiBmaWxsPSIjQkJCRUJFIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTA4XzE1Ij4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iMyIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K"
/>
`;

exports[`Avatar > Should be rendered with src pointing to a preset 1`] = `
<img
alt="Stripe"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ export const presetLineItems = {
created_at: '',
updated_at: '',
sku_code: 'BASEBHAT000000FFFFFFXXXX',
image_url:
'https://res.cloudinary.com/commercelayer/image/upload/f_auto,b_white/demo-store/skus/BASEBHAT000000FFFFFFXXXX_FLAT.png',
// image_url: 'https://res.cloudinary.com/commercelayer/image/upload/f_auto,b_white/demo-store/skus/BASEBHAT000000FFFFFFXXXX_FLAT.png',
name: 'Black Baseball Hat with White Logo',
quantity: 1,
formatted_total_amount: '34.00€',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,11 @@ export const ResourceLineItems = withSkeletonTemplate<Props>(
align='center'
rowSpan={3}
>
{imageUrl != null && (
<Avatar
size={size}
src={imageUrl as `https://${string}`}
alt={name ?? ''}
/>
)}
<Avatar
size={size}
src={imageUrl as `https://${string}`}
alt={name ?? ''}
/>
</td>
<td
className={cn('pl-4', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ function createParcelLineItems({
name,
quantity,
sku_code: code,
image_url: `https://res.cloudinary.com/commercelayer/image/upload/f_auto,b_white/demo-store/skus/${code}_FLAT.png`
image_url:
code === 'POLOMXXXFFFFFF000000SXXX'
? null
: `https://res.cloudinary.com/commercelayer/image/upload/f_auto,b_white/demo-store/skus/${code}_FLAT.png`
}
}

Expand Down
6 changes: 2 additions & 4 deletions packages/docs/src/mocks/data/bundles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { rest } from 'msw'

const bundles = rest.get(
'https://mock.localhost/api/bundles?include=sku_list.sku_list_items.sku&filter[q][code_in]=WELCOME_KIT_001',
(req, res, ctx) => {
return res(
async (req, res, ctx) => {
return await res(
ctx.status(200),
ctx.json({
data: [
Expand Down Expand Up @@ -307,8 +307,6 @@ const bundles = rest.get(
name: 'Sport Grey Unisex Hoodie Sweatshirt with Black Logo (M)',
description:
'With a large front pouch pocket and drawstrings in a matching color, this hoodie is a sure crowd-favorite. It’s soft, stylish, and perfect for the cooler evenings.',
image_url:
'https://data.commercelayer.app/seed/images/skus/HOODIEMX7F7F7F000000MXXX_FLAT.png',
pieces_per_pack: null,
weight: null,
unit_of_weight: '',
Expand Down
7 changes: 6 additions & 1 deletion packages/docs/src/stories/atoms/Avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export const Default = Template.bind({})
Default.args = {
src: 'https://data.commercelayer.app/assets/logos/glyph/white/commercelayer_glyph_white-padding.jpg',
alt: 'Commerce Layer',
border: 'none',
shape: 'circle'
}

Expand All @@ -33,6 +32,12 @@ SkuImage.args = {
alt: 'Hat'
}

/** When `src` prop is not set, then a placeholder will be shown. */
export const UndefinedSource = Template.bind({})
UndefinedSource.args = {
alt: 'The image is not present'
}

/** The image is scaled to maintain its aspect ratio while fitting within the element's content box. */
export const AspectRatio: StoryFn = (_args) => {
return (
Expand Down

0 comments on commit 8afe11c

Please sign in to comment.