Skip to content

Commit

Permalink
feat(project): use a native fallback for image service
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonLantukh committed Jul 6, 2023
1 parent 2d8e4be commit 0fc9bd6
Show file tree
Hide file tree
Showing 23 changed files with 111 additions and 397 deletions.
25 changes: 1 addition & 24 deletions src/components/Card/Card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Card from './Card';
import type { PlaylistItem } from '#types/playlist';

const item = { title: 'aa', duration: 120 } as PlaylistItem;
const itemWithImage = { title: 'This is a movie', duration: 120, shelfImage: { image: 'http://movie.jpg' } } as PlaylistItem;
const itemWithImage = { title: 'This is a movie', duration: 120, shelfImage: 'http://movie.jpg' } as PlaylistItem;

describe('<Card>', () => {
it('renders card with video title', () => {
Expand Down Expand Up @@ -35,27 +35,4 @@ describe('<Card>', () => {

expect(getByAltText('This is a movie')).toHaveStyle({ opacity: 1 });
});

it('uses the fallback image when the image fails to load', () => {
const itemWithFallbackImage = {
title: 'This is a movie',
duration: 120,
shelfImage: {
image: 'http://movie.jpg',
fallbackImage: 'http://fallback.jpg',
},
} as PlaylistItem;

const { getByAltText } = render(<Card item={itemWithFallbackImage} onClick={() => ''} />);

fireEvent.error(getByAltText('This is a movie'));

expect(getByAltText('This is a movie')).toHaveAttribute('src', 'http://fallback.jpg?width=320');
expect(getByAltText('This is a movie')).toHaveStyle({ opacity: 0 });

fireEvent.load(getByAltText('This is a movie'));

expect(getByAltText('This is a movie')).toHaveAttribute('src', 'http://fallback.jpg?width=320');
expect(getByAltText('This is a movie')).toHaveStyle({ opacity: 1 });
});
});
3 changes: 1 addition & 2 deletions src/components/Hero/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import React from 'react';
import styles from './Hero.module.scss';

import Image from '#components/Image/Image';
import type { ImageData } from '#types/playlist';

type Props = {
title: string;
description: string;
image?: ImageData;
image?: string;
};

const Hero = ({ title, description, image }: Props) => {
Expand Down
81 changes: 3 additions & 78 deletions src/components/Image/Image.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,22 @@ import Image from './Image';

describe('<Image>', () => {
test('uses the src attribute when valid', () => {
const { getByAltText } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" />);
const { getByAltText } = render(<Image image="http://image.jpg" alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=640');
});

test('tries the fallbackSrc when the image fails to load', () => {
const { getByAltText } = render(
<Image
image={{
image: 'http://image.jpg',
fallbackImage: 'http://fallback.jpg',
}}
alt="image"
/>,
);

fireEvent.error(getByAltText('image'));

expect(getByAltText('image')).toHaveAttribute('src', 'http://fallback.jpg?width=640');
});

test('updates the src attribute when changed', () => {
const { getByAltText, rerender } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=640');

rerender(<Image image={{ image: 'http://otherimage.jpg' }} alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://otherimage.jpg?width=640');
});

test('updates the src attribute when changed with the fallback image', () => {
const { getByAltText, rerender } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=640');

rerender(
<Image
image={{
image: 'http://otherimage.jpg',
fallbackImage: 'http://otherfallback.jpg',
}}
alt="image"
/>,
);

fireEvent.error(getByAltText('image'));

expect(getByAltText('image')).toHaveAttribute('src', 'http://otherfallback.jpg?width=640');
});

test('fires the onLoad callback when the image is loaded', () => {
const onLoad = vi.fn();
const { getByAltText } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" onLoad={onLoad} />);

fireEvent.load(getByAltText('image'));

expect(onLoad).toHaveBeenCalledTimes(1);
});

test('fires the onLoad callback when the fallback image is loaded', () => {
const onLoad = vi.fn();
const { getByAltText } = render(
<Image
image={{
image: 'http://image.jpg',
fallbackImage: 'http://fallback.jpg',
}}
alt="image"
onLoad={onLoad}
/>,
);
const { getByAltText } = render(<Image image="http://image.jpg" alt="image" onLoad={onLoad} />);

fireEvent.error(getByAltText('image'));
fireEvent.load(getByAltText('image'));

expect(getByAltText('image')).toHaveAttribute('src', 'http://fallback.jpg?width=640');
expect(onLoad).toHaveBeenCalledTimes(1);
});

test('changes the image width based on the given width', () => {
const { getByAltText } = render(
<Image
image={{
image: 'http://image.jpg',
fallbackImage: 'http://fallback.jpg',
}}
alt="image"
width={1280}
/>,
);
const { getByAltText } = render(<Image image="http://image.jpg" alt="image" width={1280} />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=1280');
});
Expand Down
21 changes: 4 additions & 17 deletions src/components/Image/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import classNames from 'classnames';

import styles from './Image.module.scss';

import { addQueryParams } from '#src/utils/formatting';
import type { ImageData } from '#types/playlist';

type Props = {
className?: string;
image?: ImageData;
image?: string;
onLoad?: () => void;
alt?: string;
width?: number;
Expand All @@ -19,25 +18,13 @@ const setWidth = (url: string, width: number) => {
};

const Image = ({ className, image, onLoad, alt = '', width = 640 }: Props) => {
const [imgSrc, setImgSrc] = useState(image?.image);

const handleLoad = () => {
if (onLoad) onLoad();
};

const handleError = () => {
if (image?.fallbackImage && image.fallbackImage !== image.image) {
setImgSrc(image?.fallbackImage);
}
};

useEffect(() => {
setImgSrc(image?.image);
}, [image]);

if (!imgSrc) return null;
if (!image) return null;

return <img className={classNames(className, styles.image)} src={setWidth(imgSrc, width)} onLoad={handleLoad} onError={handleError} alt={alt} />;
return <img className={classNames(className, styles.image)} src={setWidth(image, width)} onLoad={handleLoad} alt={alt} />;
};

export default React.memo(Image);
28 changes: 3 additions & 25 deletions src/components/VideoDetails/VideoDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';

import VideoDetails from './VideoDetails';

Expand All @@ -11,7 +11,7 @@ describe('<VideoDetails>', () => {
description="Video description"
primaryMetadata="Primary metadata string"
secondaryMetadata={<strong>Secondary metadata string</strong>}
image={{ image: 'http://image.jpg' }}
image="http://image.jpg"
startWatchingButton={<button>Start watching</button>}
shareButton={<button>share</button>}
favoriteButton={<button>favorite</button>}
Expand All @@ -29,7 +29,7 @@ describe('<VideoDetails>', () => {
description="Video description"
primaryMetadata="Primary metadata string"
secondaryMetadata={<strong>Secondary metadata string</strong>}
image={{ image: 'http://image.jpg' }}
image="http://image.jpg"
startWatchingButton={<button>Start watching</button>}
shareButton={<button>share</button>}
favoriteButton={<button>favorite</button>}
Expand All @@ -39,26 +39,4 @@ describe('<VideoDetails>', () => {

expect(getByAltText('Test video')).toHaveAttribute('src', 'http://image.jpg?width=1280');
});

test('renders the fallback image when the image fails to load', () => {
const { getByAltText } = render(
<VideoDetails
title="Test video"
description="Video description"
primaryMetadata="Primary metadata string"
secondaryMetadata={<strong>Secondary metadata string</strong>}
image={{ image: 'http://image.jpg', fallbackImage: 'http://fallback.jpg' }}
startWatchingButton={<button>Start watching</button>}
shareButton={<button>share</button>}
favoriteButton={<button>favorite</button>}
trailerButton={<button>play trailer</button>}
/>,
);

expect(getByAltText('Test video')).toHaveAttribute('src', 'http://image.jpg?width=1280');

fireEvent.error(getByAltText('Test video'));

expect(getByAltText('Test video')).toHaveAttribute('src', 'http://fallback.jpg?width=1280');
});
});
3 changes: 1 addition & 2 deletions src/components/VideoDetails/VideoDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import styles from './VideoDetails.module.scss';
import CollapsibleText from '#components/CollapsibleText/CollapsibleText';
import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint';
import Image from '#components/Image/Image';
import type { ImageData } from '#types/playlist';
import { testId } from '#src/utils/common';

type Props = {
title: string;
description: string;
primaryMetadata: React.ReactNode;
secondaryMetadata?: React.ReactNode;
image?: ImageData;
image?: string;
startWatchingButton: React.ReactNode;
shareButton: React.ReactNode;
favoriteButton: React.ReactNode;
Expand Down
4 changes: 2 additions & 2 deletions src/components/VideoLayout/VideoLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import VideoDetailsInline from '#components/VideoDetailsInline/VideoDetailsInlin
import VideoList from '#components/VideoList/VideoList';
import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint';
import { testId } from '#src/utils/common';
import type { ImageData, Playlist, PlaylistItem } from '#types/playlist';
import type { Playlist, PlaylistItem } from '#types/playlist';
import type { AccessModel } from '#types/Config';

type FilterProps = {
Expand All @@ -29,7 +29,7 @@ type LoadMoreProps = {
type VideoDetailsProps = {
title: string;
description: string;
image?: ImageData;
image?: string;
primaryMetadata: React.ReactNode;
secondaryMetadata?: React.ReactNode;
shareButton: React.ReactNode;
Expand Down
29 changes: 15 additions & 14 deletions src/hooks/usePlanByEpg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { startOfDay, startOfToday, startOfTomorrow } from 'date-fns';

import type { EpgChannel } from '#src/services/epg.service';
import { is12HourClock } from '#src/utils/datetime';
import { getImage } from '#src/utils/image';

const isBaseTimeFormat = is12HourClock();

Expand All @@ -13,24 +14,24 @@ const isBaseTimeFormat = is12HourClock();
const usePlanByEpg = (channels: EpgChannel[], sidebarWidth: number, itemHeight: number, highlightColor?: string | null, backgroundColor?: string | null) => {
const [epgChannels, epgPrograms] = useMemo(() => {
return [
channels.map((channel) => ({
uuid: channel.id,
logo: channel.channelLogoImage?.image || '',
channelLogoImage: channel.channelLogoImage,
backgroundImage: channel.backgroundImage,
channels.map(({ id, channelLogoImage, backgroundImage }) => ({
uuid: id,
logo: getImage(channelLogoImage),
channelLogoImage: getImage(channelLogoImage),
backgroundImage: getImage(backgroundImage),
})),
channels.flatMap((channel) =>
channel.programs.map((program) => ({
channel.programs.map(({ id, title, shelfImage, backgroundImage, description, endTime, startTime }) => ({
channelUuid: channel.id,
id: program.id,
title: program.title,
image: program.shelfImage?.image || '',
id: id,
title,
image: getImage(shelfImage),
// programs have the same shelfImage/backgroundImage (different API)
shelfImage: program.shelfImage,
backgroundImage: program.backgroundImage,
description: program.description || '',
till: program.endTime,
since: program.startTime,
shelfImage: getImage(shelfImage),
backgroundImage: getImage(backgroundImage),
description: description || '',
till: endTime,
since: startTime,
})),
),
];
Expand Down
4 changes: 2 additions & 2 deletions src/pages/LegacySeries/LegacySeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import ShareButton from '#components/ShareButton/ShareButton';
import FavoriteButton from '#src/containers/FavoriteButton/FavoriteButton';
import Button from '#components/Button/Button';
import PlayTrailer from '#src/icons/PlayTrailer';
import type { PlaylistItem, ImageData } from '#types/playlist';
import type { PlaylistItem } from '#types/playlist';
import useQueryParam from '#src/hooks/useQueryParam';
import Loading from '#src/pages/Loading/Loading';
import usePlaylist from '#src/hooks/usePlaylist';
Expand Down Expand Up @@ -108,7 +108,7 @@ const LegacySeries = () => {
const pageTitle = `${selectedItem.title} - ${siteName}`;
const pageDescription = selectedItem?.description || '';
const canonicalUrl = `${window.location.origin}${legacySeriesURL({ episodeId: episode?.mediaid, seriesId })}`;
const backgroundImage = (selectedItem.backgroundImage as ImageData) || undefined;
const backgroundImage = (selectedItem.backgroundImage as string) || undefined;

const primaryMetadata = episode
? formatVideoMetaString(episode, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { generateMovieJSONLD } from '#src/utils/structuredData';
import type { ScreenComponent } from '#types/screens';
import type { Playlist } from '#types/playlist';
import Loading from '#src/pages/Loading/Loading';
import { getImage } from '#src/utils/image';

const PlaylistLiveChannels: ScreenComponent<Playlist> = ({ data: { feedid, playlist } }) => {
const { t } = useTranslation('epg');
Expand Down Expand Up @@ -57,7 +58,7 @@ const PlaylistLiveChannels: ScreenComponent<Playlist> = ({ data: { feedid, playl
return {
title: program.title,
description: program.description || '',
image: program.backgroundImage,
image: getImage(program.backgroundImage),
canWatch: isLive || (isVod && isWatchableFromBeginning),
canWatchFromBeginning: isEntitled && isLive && isWatchableFromBeginning,
};
Expand All @@ -66,7 +67,7 @@ const PlaylistLiveChannels: ScreenComponent<Playlist> = ({ data: { feedid, playl
return {
title: channel?.title || '',
description: channel?.description || '',
image: channel?.backgroundImage,
image: getImage(channel?.backgroundImage),
canWatch: true,
canWatchFromBeginning: false,
};
Expand Down
Loading

0 comments on commit 0fc9bd6

Please sign in to comment.