Skip to content

Commit

Permalink
feat(home): add hero shelf swipe for all breakpoints
Browse files Browse the repository at this point in the history
refactor(home): make swipeslider more stable

refactor(home): optimize heroshelf landscape
  • Loading branch information
royschut committed Nov 18, 2024
1 parent 932a362 commit 3a3b814
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 99 deletions.
18 changes: 14 additions & 4 deletions packages/ui-react/src/components/HeroShelf/HeroShelf.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ $desktop-max-height: 700px;
$desktop-min-height: 275px;

$tablet-height: 70vw;
$tablet-min-height: 375px;
$tablet-min-height: 550px;

$mobile-height: 70vh;
$mobile-min-height: 450px;
Expand Down Expand Up @@ -47,13 +47,13 @@ $mobile-landscape-height: 100vh;
}

@include responsive.mobile-only() {
height: calc($mobile-height - variables.$header-height - var(--safe-area-top, 0));
height: calc($mobile-height - variables.$header-height - var(--safe-area-top, 0px));
min-height: calc($mobile-min-height - variables.$header-height);
margin-top: var(--safe-area-top, 0);
}

@include responsive.mobile-only-landscape() {
height: calc($mobile-landscape-height - variables.$header-height);
min-height: initial;
padding: 0;
}
}
Expand Down Expand Up @@ -113,6 +113,7 @@ $mobile-landscape-height: 100vh;

@include responsive.mobile-only-landscape() {
height: $mobile-landscape-height;
min-height: initial;
}
}

Expand Down Expand Up @@ -178,7 +179,14 @@ $mobile-landscape-height: 100vh;
}
}

.metadataMobile {
.swipeSlider {
display: flex;
align-items: flex-end;
width: 100%;
height: 100%;
}

.swipeSliderMobile {
width: 100%;
height: 100%;
}
Expand All @@ -188,6 +196,7 @@ $mobile-landscape-height: 100vh;
display: flex;
flex-direction: column;
gap: 24px;
width: 46%;
max-width: 46%;
padding-left: calc(variables.$base-spacing * 4);

Expand Down Expand Up @@ -225,6 +234,7 @@ $mobile-landscape-height: 100vh;

@include responsive.mobile-only-landscape() {
max-width: 70%;
min-height: initial;
padding: 0 calc(variables.$base-spacing * 3) calc(variables.$base-spacing * 3) calc(variables.$base-spacing * 3);
}

Expand Down
148 changes: 97 additions & 51 deletions packages/ui-react/src/components/HeroShelf/HeroShelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import styles from './HeroShelf.module.scss';
import HeroShelfMetadata from './HeroShelfMetadata';
import HeroShelfBackground from './HeroShelfBackground';
import HeroShelfPagination from './HeroShelfPagination';
import HeroShelfMetadataMobile from './HeroShelfMetadataMobile';
import HeroSwipeSlider from './HeroSwipeSlider';

type Props = {
playlist: Playlist;
Expand All @@ -30,21 +30,25 @@ const HeroShelf = ({ playlist, loading = false, error = null }: Props) => {
const posterRef = useRef<HTMLDivElement>(null);
const [direction, setDirection] = useState<'left' | 'right' | null>(null);
const [animationPhase, setAnimationPhase] = useState<'init' | 'start' | 'end' | null>(null);
const [isSwipeAnimation, setIsSwipeAnimation] = useState(false);

useScrolledDown(50, isMobile ? 200 : 700, (progress: number) => {
if (posterRef.current) posterRef.current.style.opacity = `${Math.max(1 - progress, isMobile ? 0 : 0.1)}`;
});

const slideTo = (toIndex: number) => {
const slideTo = (toIndex: number, isSwiping = false) => {
if (animationPhase) return;

setNextIndex(toIndex);
setDirection(toIndex > index ? 'right' : 'left');
setAnimationPhase('init');
setIsSwipeAnimation(isSwiping);
};

const slideLeft = () => slideTo(index - 1);
const slideRight = () => slideTo(index + 1);
const handleSlideLeft = () => slideTo(index - 1);
const handleSlideRight = () => slideTo(index + 1);
const handleSwipeLeft = () => slideTo(index - 1, true);
const handleSwipeRight = () => slideTo(index + 1, true);

const handleBackgroundAnimationEnd: TransitionEventHandler = useCallback(
(event) => {
Expand All @@ -66,47 +70,67 @@ const HeroShelf = ({ playlist, loading = false, error = null }: Props) => {
setIndex(nextIndex);
setDirection(null);
setAnimationPhase(null);
setIsSwipeAnimation(false);
}
}, [animationPhase, direction, nextIndex]);

const isAnimating = animationPhase === 'start' || animationPhase === 'end';
const directionFactor = direction === 'left' ? 1 : direction === 'right' ? -1 : 0;

// Background animation
const backgroundX = isMobile ? 10 : 40;
const backgroundCurrentStyle: CSSProperties = {
transform: `scale(1.2) translateX(${isAnimating ? backgroundX * directionFactor : 0}px)`,
opacity: isAnimating ? 0 : 1,
transition: isAnimating ? `opacity ${isMobile ? 0.3 : 0.1}s ease-out, transform 0.3s ease-in` : 'none',
};
const backgroundAltStyle: CSSProperties = {
transform: `scale(1.2) translateX(${animationPhase === 'init' ? backgroundX * directionFactor * -1 : 0}px)`,
opacity: isAnimating ? 1 : 0,
transition: isAnimating ? 'opacity 0.3s ease-out, transform 0.3s ease-out' : 'none',
};
const getBackgroundStyle = (side?: 'left' | 'right') => {
const backgroundX = isMobile ? 10 : 40;

// Metadata animation
const left = 60;
const metadataCurrentStyle: CSSProperties = {
left: isAnimating && direction ? left * directionFactor : 0,
opacity: isAnimating ? 0 : 1,
transition: isAnimating ? 'opacity 0.15s ease-out, left 0.15s ease-out' : 'none',
pointerEvents: isAnimating ? 'none' : 'initial',
if (side == 'left') {
return {
transform: `scale(1.2) translateX(${animationPhase === 'init' ? backgroundX * -1 : 0}px)`,
opacity: isAnimating ? 1 : 0,
transition: isAnimating ? 'opacity 0.3s ease-out, transform 0.3s ease-out' : 'none',
};
}
if (side == 'right') {
return {
transform: `scale(1.2) translateX(${animationPhase === 'init' ? backgroundX : 0}px)`,
opacity: isAnimating ? 1 : 0,
transition: isAnimating ? 'opacity 0.3s ease-out, transform 0.3s ease-out' : 'none',
};
}
return {
transform: `scale(1.2) translateX(${isAnimating ? backgroundX * directionFactor : 0}px)`,
opacity: isAnimating ? 0 : 1,
transition: isAnimating ? `opacity ${isMobile ? 0.3 : 0.1}s ease-out, transform 0.3s ease-in` : 'none',
};
};

const metadataAltStyle: CSSProperties = {
left: animationPhase === 'init' ? left * directionFactor * -1 : 0,
opacity: isAnimating ? 1 : 0,
transition: isAnimating ? 'opacity 0.2s ease-out, left 0.2s ease-out' : 'none',
pointerEvents: 'none',
const getMetadataStyle = (side?: 'left' | 'right', isSwiping = false): CSSProperties => {
if (side === 'left') {
return {
left: isSwiping || isSwipeAnimation ? '-100%' : animationPhase === 'init' ? -60 : 0,
opacity: isSwiping || isSwipeAnimation || isAnimating ? 1 : 0,
transition: isAnimating ? 'opacity 0.2s ease-out, left 0.2s ease-out' : 'none',
pointerEvents: 'none',
};
}
if (side === 'right') {
return {
left: isSwiping || isSwipeAnimation ? '100%' : animationPhase === 'init' ? 60 : 0,
opacity: isSwiping || isSwipeAnimation || isAnimating ? 1 : 0,
transition: isAnimating ? 'opacity 0.2s ease-out, left 0.2s ease-out' : 'none',
pointerEvents: 'none',
};
}
return {
left: isAnimating && direction ? 60 * directionFactor : 0,
opacity: isAnimating ? 0 : 1,
transition: isAnimating ? 'opacity 0.15s ease-out, left 0.15s ease-out' : 'none',
pointerEvents: isAnimating ? 'none' : 'initial',
};
};

const item = playlist.playlist[index];
const leftItem = playlist.playlist[nextIndex < index ? nextIndex : index - 1] || null;
const rightItem = playlist.playlist[nextIndex > index ? nextIndex : index + 1] || null;

const renderedItem = animationPhase !== 'end' ? item : direction === 'right' ? leftItem : rightItem;
const altItem = direction === 'right' ? rightItem : leftItem;

if (error || !playlist?.playlist) return <h2 className={styles.error}>Could not load items</h2>;

Expand All @@ -116,14 +140,14 @@ const HeroShelf = ({ playlist, loading = false, error = null }: Props) => {
<div className={styles.background} id="background">
<HeroShelfBackground
item={leftItem}
style={backgroundAltStyle}
style={getBackgroundStyle('left')}
key={renderedItem?.mediaid === leftItem?.mediaid ? 'left-item' : leftItem?.mediaid}
hidden={direction !== 'left'}
/>
<HeroShelfBackground item={renderedItem} style={backgroundCurrentStyle} key={renderedItem?.mediaid} onTransitionEnd={handleBackgroundAnimationEnd} />
<HeroShelfBackground item={renderedItem} style={getBackgroundStyle()} key={renderedItem?.mediaid} onTransitionEnd={handleBackgroundAnimationEnd} />
<HeroShelfBackground
item={rightItem}
style={backgroundAltStyle}
style={getBackgroundStyle('right')}
key={renderedItem?.mediaid === rightItem?.mediaid ? 'right-item' : rightItem?.mediaid}
hidden={direction !== 'right'}
/>
Expand All @@ -135,32 +159,54 @@ const HeroShelf = ({ playlist, loading = false, error = null }: Props) => {
className={classNames(styles.chevron, styles.chevronLeft)}
aria-label={t('slide_previous')}
disabled={!leftItem}
onClick={leftItem ? slideLeft : undefined}
onClick={leftItem ? handleSlideLeft : undefined}
>
<Icon icon={ChevronLeft} />
</button>
{isMobile ? (
<HeroShelfMetadataMobile
loading={loading}
item={item}
rightItem={rightItem}
leftItem={leftItem}
playlistId={playlist.feedid}
direction={direction}
onSlideLeft={slideLeft}
onSlideRight={slideRight}
/>
) : (
<>
<HeroShelfMetadata item={renderedItem} loading={loading} playlistId={playlist.feedid} style={metadataCurrentStyle} />
<HeroShelfMetadata item={altItem} loading={loading} playlistId={playlist.feedid} style={metadataAltStyle} hidden={!direction} />
</>
)}
<HeroSwipeSlider
direction={direction}
isSwipeAnimation={isSwipeAnimation}
loading={loading}
onSwipeLeft={handleSwipeLeft}
onSwipeRight={handleSwipeRight}
hasLeftItem={!!leftItem}
hasRightItem={!!rightItem}
renderLeftItem={(isSwiping: boolean) => (
<HeroShelfMetadata
loading={loading}
item={leftItem}
playlistId={playlist.feedid}
style={getMetadataStyle('left', isSwiping)}
key={renderedItem?.mediaid === leftItem?.mediaid ? 'left-item' : leftItem?.mediaid}
hidden={direction !== 'left' && !isSwiping}
/>
)}
renderItem={() => (
<HeroShelfMetadata
loading={loading}
item={renderedItem}
playlistId={playlist.feedid}
isMobile={isMobile}
key={renderedItem?.mediaid}
style={getMetadataStyle()}
/>
)}
renderRightItem={(isSwiping: boolean) => (
<HeroShelfMetadata
loading={loading}
item={rightItem}
playlistId={playlist.feedid}
style={getMetadataStyle('right', isSwiping)}
key={renderedItem?.mediaid === rightItem?.mediaid ? 'right-item' : rightItem?.mediaid}
hidden={direction !== 'right' && !isSwiping}
/>
)}
/>
<button
className={classNames(styles.chevron, styles.chevronRight)}
aria-label={t('slide_next')}
disabled={!rightItem}
onClick={rightItem ? slideRight : undefined}
onClick={rightItem ? handleSlideRight : undefined}
>
<Icon icon={ChevronRight} />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TruncatedText from '../TruncatedText/TruncatedText';
import StartWatchingButton from '../../containers/StartWatchingButton/StartWatchingButton';
import Button from '../Button/Button';
import Icon from '../Icon/Icon';
import useBreakpoint, { Breakpoint } from '../../hooks/useBreakpoint';

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

Expand All @@ -21,17 +22,18 @@ const HeroShelfMetadata = ({
playlistId,
style,
hidden,
isMobile,
}: {
item: PlaylistItem | null;
loading: boolean;
playlistId: string | undefined;
style?: CSSProperties;
style: CSSProperties;
hidden?: boolean;
isMobile?: boolean;
}) => {
const navigate = useNavigate();
const { t } = useTranslation('common');
const breakpoint = useBreakpoint();
const isMobile = breakpoint <= Breakpoint.sm;

if (!item) return null;

Expand Down
Loading

0 comments on commit 3a3b814

Please sign in to comment.