Skip to content

Commit

Permalink
feat: implement tile-slider dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer authored Jul 18, 2024
1 parent 892f41b commit d58f1cb
Show file tree
Hide file tree
Showing 14 changed files with 612 additions and 877 deletions.
1 change: 1 addition & 0 deletions packages/ui-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@adyen/adyen-web": "^5.66.1",
"@videodock/tile-slider": "^2.0.0",
"classnames": "^2.5.1",
"date-fns": "^2.30.0",
"dompurify": "^2.5.5",
Expand Down
6 changes: 0 additions & 6 deletions packages/ui-react/src/components/Card/Card.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,6 @@
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.3s ease-out;

&.visible {
opacity: 1;
}
}
}

Expand Down
13 changes: 0 additions & 13 deletions packages/ui-react/src/components/Card/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import { axe } from 'vitest-axe';
import { fireEvent } from '@testing-library/react';
import type { PlaylistItem } from '@jwp/ott-common/types/playlist';

import { renderWithRouter } from '../../../test/utils';
Expand All @@ -26,18 +25,6 @@ describe('<Card>', () => {
expect(getByAltText('')).toHaveAttribute('src', 'http://movie.jpg?width=320');
});

it('makes the image visible after load', () => {
const { getByAltText } = renderWithRouter(<Card item={itemWithImage} url="https://test.dummy.jwplayer.com" />);
const image = getByAltText(''); // Image alt is intentionally empty for a11y

expect(image).toHaveAttribute('src', 'http://movie.jpg?width=320');
expect(image).toHaveStyle({ opacity: 0 });

fireEvent.load(image);

expect(image).toHaveStyle({ opacity: 1 });
});

it('should render anchor tag', () => {
const { container } = renderWithRouter(<Card item={itemWithImage} url="https://test.dummy.jwplayer.com" />);
expect(container).toMatchSnapshot();
Expand Down
14 changes: 5 additions & 9 deletions packages/ui-react/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useState } from 'react';
import React, { memo } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
Expand Down Expand Up @@ -57,7 +57,6 @@ function Card({
} = useTranslation(['common', 'video']);
// t('play_item')

const [imageLoaded, setImageLoaded] = useState(false);
const cardClassName = classNames(styles.card, {
[styles.featured]: featured,
[styles.disabled]: disabled,
Expand All @@ -66,16 +65,13 @@ function Card({
const posterClassNames = classNames(styles.poster, aspectRatioClass, {
[styles.current]: isCurrent,
});
const posterImageClassNames = classNames(styles.posterImage, {
[styles.visible]: imageLoaded,
});

const isSeriesItem = isSeries(item);
const isLive = mediaStatus === MediaStatus.LIVE || isLiveChannel(item);
const isScheduled = mediaStatus === MediaStatus.SCHEDULED;

const renderTag = () => {
if (loading || disabled || !title) return null;
if (loading || !title) return null;

if (isSeriesItem) {
return <div className={styles.tag}>{t('video:series')}</div>;
Expand Down Expand Up @@ -107,7 +103,7 @@ function Card({
tabIndex={disabled ? -1 : tabIndex}
data-testid={testId(title)}
>
{!featured && !disabled && (
{!featured && (
<div className={styles.titleContainer}>
{heading}
{!!scheduledStart && isLiveEvent(item) && (
Expand All @@ -116,10 +112,10 @@ function Card({
</div>
)}
<div className={posterClassNames}>
<Image className={posterImageClassNames} image={image} width={featured ? 640 : 320} onLoad={() => setImageLoaded(true)} alt="" />
<Image className={styles.posterImage} image={image} width={featured ? 640 : 320} alt="" />
{!loading && (
<div className={styles.meta}>
{featured && !disabled && heading}
{featured && heading}
<div className={styles.tags}>
{isLocked && (
<div className={classNames(styles.tag, styles.lock)} aria-label={t('card_lock')} role="img">
Expand Down
56 changes: 49 additions & 7 deletions packages/ui-react/src/components/Image/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import classNames from 'classnames';
import React, { useEffect, useState } from 'react';
import { createURL } from '@jwp/ott-common/src/utils/urlFormatting';
import { logInfo } from '@jwp/ott-common/src/logger';

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

Expand All @@ -16,14 +16,56 @@ const setWidth = (url: string, width: number) => {
return createURL(url, { width });
};

const cache = new Map();

const resolveImageURL = async (imgUrl: string, width: number) => {
const requestUrl = setWidth(imgUrl, width);
let url = requestUrl;

if (cache.has(requestUrl)) {
return cache.get(requestUrl);
}

try {
const response = await fetch(requestUrl);

// if redirected, cache and return resolved URL
if (response.redirected) {
url = response.url.replace('-1920', `-${width}`);
}

cache.set(requestUrl, url);
} catch (error) {
logInfo('Image', 'Failed to fetch image', { error, url });
}

return url;
};

const Image = ({ className, image, onLoad, alt = '', width = 640 }: Props) => {
const handleLoad = () => {
if (onLoad) onLoad();
};
const [src, setSrc] = useState<string | null>(null);

useEffect(() => {
if (!image) return;

const loadImage = async () => {
const resolvedImage = await resolveImageURL(image, width);

setSrc(resolvedImage);
onLoad?.();
};

if (__mode__ === 'test') {
setSrc(setWidth(image, width));
onLoad?.();
} else {
loadImage();
}
}, [image, width, onLoad]);

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

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

export default React.memo(Image);
57 changes: 43 additions & 14 deletions packages/ui-react/src/components/Shelf/Shelf.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@use '@jwp/ott-ui-react/src/styles/mixins/responsive';
@use '@jwp/ott-ui-react/src/styles/variables';
@use '@jwp/ott-ui-react/src/styles/theme';

.shelf {
Expand All @@ -6,13 +8,9 @@
font-family: var(--body-alt-font-family);

&:hover,
&:focus-within
{
.chevron {
&:focus-within {
.chevron:not(:disabled) {
opacity: 1;
&.disabled {
opacity: 0.3;
}
}
}
}
Expand All @@ -23,7 +21,7 @@
margin-bottom: 12px;
background-color: theme.$card-loading-bg-color;
border-radius: 5px;
};
}

.title {
width: 100%;
Expand All @@ -38,6 +36,26 @@
text-overflow: ellipsis;
}

.slider {
position: static;
overflow: visible;

li > a {
white-space: initial;
}

@include responsive.tablet-and-larger() {
li {
opacity: 1;
transition: opacity 0.2s ease;

&:global(.TileSlider--hidden) {
opacity: 0.5;
}
}
}
}

.chevron {
display: flex;
flex-wrap: wrap;
Expand All @@ -47,23 +65,33 @@
height: 44px;
outline-color: var(--highlight-color, white);
cursor: pointer;
filter: drop-shadow(1px 1px 2px variables.$black);
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
appearance: none;

> svg {
width: 30px;
height: 30px;
width: 40px;
height: 40px;
}
&.disabled {
cursor: default;
&:hover {
transform: none;
}

&:disabled {
opacity: 0.3;
pointer-events: none;
}

&:hover {
transform: scale(1.2);
}
}

.dots {
position: relative;
display: flex;
justify-content: center;
width: 100%;
margin-top: 12px;
}

.dot {
display: inline-block;
width: 10px;
Expand All @@ -72,6 +100,7 @@
background-color: rgba(254, 254, 254, 0.2);
border-radius: 50%;
transition: all 200ms ease;
will-change: transform;

&.active {
background-color: var(--primary-color);
Expand Down
Loading

0 comments on commit d58f1cb

Please sign in to comment.