Skip to content

Commit

Permalink
Merge pull request #161 from sima-land/160-product-info-broken-image
Browse files Browse the repository at this point in the history
#160 ProductInfo: реализовать вывод заглушки вместо изображения товара
  • Loading branch information
krutoo authored May 25, 2023
2 parents 2f228df + 9851e7f commit d67fbbf
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 5 deletions.
48 changes: 46 additions & 2 deletions src/common/components/product-info/__stories__/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { action } from '@storybook/addon-actions';
import { ProductInfo, Parts } from '..';
import { Badge, BadgeProps } from '../../badge';
import { Stepper } from '@sima-land/ui-nucleons/stepper';
import FavoriteSVG from '@sima-land/ui-quarks/icons/24x24/Stroked/favorite';
import QuickView2SVG from '@sima-land/ui-quarks/icons/24x24/Stroked/quick-view-2';
import Camera2SVG from '@sima-land/ui-quarks/icons/24x24/Stroked/camera-2';
import on from '@sima-land/ui-nucleons/helpers/on';

export default {
title: 'common/ProductInfo',
Expand All @@ -29,7 +30,7 @@ const badges: BadgeProps[] = [

const data = {
name: 'Ножницы портновские, с прорезиненной ручкой, 20 см, цвет чёрный/серый',
imageSrc: 'https://picsum.photos/240/360',
imageSrc: 'https://loremflickr.com/240/360',
url: 'https://www.sima-land.ru',
price: 99876543.21,
oldPrice: 99987654.32,
Expand Down Expand Up @@ -500,3 +501,46 @@ export const Adult = () => (
);

Adult.storyName = 'Товар для взрослых';

export function TestBrokenImage() {
const brokenSrc = 'https://kasjbgakjbsg.asgkjabsghj';
const [imageSrc, setImageSrc] = useState(data.imageSrc);

useEffect(() => {
const off = on<KeyboardEvent>(window, 'keydown', e => {
if (e.code === 'KeyR') {
setImageSrc(src => (src === brokenSrc ? data.imageSrc : brokenSrc));
}
});

return off;
}, []);

return (
<div style={{ width: '260px' }}>
<ProductInfo>
<Parts.Image src={imageSrc} href={data.url}>
<Parts.ImageButton
icon={FavoriteSVG}
hint='Добавить в избранное'
data-testid='favorite-button'
/>
<Parts.ImageButton
icon={QuickView2SVG}
hint='Быстрый просмотр'
data-testid='quick-view-button'
/>
</Parts.Image>

<Parts.Prices
price={data.price}
oldPrice={data.oldPrice}
currencyGrapheme={data.currencyGrapheme}
/>

<Parts.Title href={data.url}>{data.name}</Parts.Title>
</ProductInfo>
<p>Нажмите R на клавиатуре чтобы переключить картинку на битую и обратно</p>
</div>
);
}
47 changes: 47 additions & 0 deletions src/common/components/product-info/__test__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,51 @@ describe('ProductInfo', () => {
expect(getByTestId('product-info:prices').textContent).toContain('Test reason');
});
});

it('should render stub for broken image', () => {
const { container, getByTestId } = render(
<ProductInfo>
<Parts.Image src={data.imageSrc} href={data.url} opacity={0.72}>
<Parts.ImageButton icon={QuickViewSVG} data-testid='quick-view-button' />
</Parts.Image>

<Parts.Prices
price={data.price}
oldPrice={data.oldPrice}
currencyGrapheme={data.currencyGrapheme}
unavailableReason='Test reason'
/>

<Parts.Title href={data.url}>{data.name}</Parts.Title>
</ProductInfo>,
);

expect(container.querySelectorAll('.broken-icon')).toHaveLength(0);
fireEvent.error(getByTestId('product-info:image'));
expect(container.querySelectorAll('.broken-icon')).toHaveLength(1);
});

it('should render stub for broken adult image', () => {
const { container, getByTestId } = render(
<ProductInfo restriction='adult'>
<Parts.Image src={data.imageSrc} href={data.url} opacity={0.72}>
<Parts.ImageButton icon={QuickViewSVG} data-testid='quick-view-button' />
</Parts.Image>

<Parts.Prices
price={data.price}
oldPrice={data.oldPrice}
currencyGrapheme={data.currencyGrapheme}
unavailableReason='Test reason'
/>

<Parts.Title href={data.url}>{data.name}</Parts.Title>
</ProductInfo>,
);

expect(container.querySelectorAll('.broken-icon')).toHaveLength(0);
fireEvent.error(getByTestId('product-info:adult-image'));
expect(container.querySelectorAll('.broken-icon')).toHaveLength(0);
expect(getByTestId('product-info:adult-image').classList.contains('broken')).toBe(true);
});
});
13 changes: 10 additions & 3 deletions src/common/components/product-info/parts.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, ReactNode, useContext } from 'react';
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { Link, LinkProps } from '@sima-land/ui-nucleons/link';
import { HintProps } from '@sima-land/ui-nucleons/hint-deprecated';
import { Price } from '@sima-land/ui-nucleons/price';
Expand All @@ -9,6 +9,7 @@ import { BadgeList, BadgeListProps } from '../badge-list';
import { ProductInfoContext } from './utils';
import { RatingCounter, RatingCounterProps } from '../rating-counter';
import AdultSVG from '../../icons/eighteen-plus.svg';
import BrokenSVG from '../../icons/image-broken.svg';
import classnames from 'classnames/bind';
import styles from './product-info.module.scss';

Expand Down Expand Up @@ -86,15 +87,19 @@ const ImageButton = ({
const Image = ({ src, alt, href, onClick, children, opacity }: ImageProps) => {
const { restriction } = useContext(ProductInfoContext);
const defaultOpacity = restriction ? 0.4 : undefined;
const [broken, setBroken] = useState(false);

useEffect(() => setBroken(false), [src]);

return (
<ImageOverlay className={cx('image-overlay')}>
{restriction === 'adult' ? (
<>
<img
onError={() => setBroken(true)}
alt={alt}
src={src}
className={cx('image', 'adult')}
className={cx('image', 'adult', { broken })}
data-testid='product-info:adult-image'
/>
<AdultSVG className={cx('adult-icon')} />
Expand All @@ -108,12 +113,14 @@ const Image = ({ src, alt, href, onClick, children, opacity }: ImageProps) => {
data-testid='product-info:image-link'
>
<img
onError={() => setBroken(true)}
alt={alt}
src={src}
className={cx('image')}
className={cx('image', { broken })}
style={{ opacity: typeof opacity === 'number' ? opacity : defaultOpacity }}
data-testid='product-info:image'
/>
{broken && <BrokenSVG className={cx('broken-icon')} />}
</a>

{children && <div className={cx('image-buttons')}>{children}</div>}
Expand Down
9 changes: 9 additions & 0 deletions src/common/components/product-info/product-info.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
object-fit: contain;
width: 100%;
height: 100%;
&.broken {
display: none;
}
&.adult {
position: absolute;
top: 0;
Expand All @@ -51,6 +54,12 @@
fill: colors.$basic-gray38;
}

.broken-icon {
position: absolute;
width: 100%;
height: 100%;
}

.image-buttons {
position: absolute;
top: 0;
Expand Down
Loading

0 comments on commit d67fbbf

Please sign in to comment.