Skip to content

Commit

Permalink
oct-2157: video bar navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
jmikolajczyk committed Oct 30, 2024
1 parent 6620f3d commit 45a8ee9
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
.constraintsWrapper {
display: flex;
overflow: hidden;
margin-top: 4rem;
margin-top: 0.8rem;

@media #{$tablet-up} {
margin-top: 4rem;
}

@media #{$desktop-up} {
margin-top: 1.2rem;
Expand All @@ -23,3 +27,21 @@
right: 1.6rem;
background-color: $color-octant-grey6;
}

.arrows {
padding-top: 3.2rem;
padding-left: 2.4rem;

@media #{$tablet-up} {
padding-top: 0;
padding-left: 1.4rem;
}
}

.arrowLeft {
margin-right: 1.6rem;

@media #{$tablet-up} {
margin-right: 0.8rem;
}
}
99 changes: 90 additions & 9 deletions client/src/components/Home/HomeGridVideoBar/HomeGridVideoBar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { motion } from 'framer-motion';
import React, { FC, useRef, useState } from 'react';
import { motion, useMotionValue, useMotionValueEvent } from 'framer-motion';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

import VideoTile from 'components/Home/HomeGridVideoBar/VideoTile';
import GridTile from 'components/shared/Grid/GridTile';
import NavigationArrows from 'components/shared/NavigationArrows';
import Button from 'components/ui/Button';
import Svg from 'components/ui/Svg';
import useMediaQuery from 'hooks/helpers/useMediaQuery';
import useVimeoVideos from 'hooks/queries/useVimeoVideos';
import useSettingsStore from 'store/settings/store';
import { cross } from 'svg/misc';
Expand All @@ -17,45 +19,124 @@ const HomeGridVideoBar: FC<HomeGridVideoBarProps> = ({ className }) => {
const { t } = useTranslation('translation', {
keyPrefix: 'components.home.homeGridVideoBar',
});
const x = useMotionValue(0);
const { isMobile, isTablet } = useMediaQuery();
const { data } = useVimeoVideos();
const constraintsRef = useRef<HTMLDivElement>(null);
const videosWrapperRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const tilesRef = useRef<HTMLDivElement[]>([]);
const [isNextButtonActive, setIsNextButtonActive] = useState(false);
const [isPrevButtonActive, setIsPrevButtonActive] = useState(false);
const containerBoundingClientRect = constraintsRef.current?.getBoundingClientRect();
const videosWrapperBoundingClientRect = videosWrapperRef.current?.getBoundingClientRect();
const containerLeft = containerBoundingClientRect?.left ?? 0;
const containerWidth = containerBoundingClientRect?.width ?? 0;
const videosWrapperWidth = videosWrapperBoundingClientRect?.width ?? 0;
const maxMotionValue = videosWrapperWidth - containerWidth;

const { setShowHelpVideos } = useSettingsStore(state => ({
setShowHelpVideos: state.setShowHelpVideos,
}));

useMotionValueEvent(x, 'change', nextX => {
setIsPrevButtonActive(nextX < 0);
setIsNextButtonActive(Math.ceil(Math.abs(nextX)) < maxMotionValue);
});

const moveVideoBar = (isPrev?: boolean) => {
if (!constraintsRef.current || !videosWrapperRef.current) {
return;
}
const tilesGap = 24;
const tileWidth = isMobile || isTablet ? 280 : 392;

const amountOfVisibleTiles = Math.floor(containerWidth / (tileWidth + tilesGap));
const numberOfHiddenTilesLeft = Math.ceil(Math.abs(x.get()) / (tileWidth + tilesGap));
const numberOfHiddenTilesRight = Math.ceil(
Math.abs(x.get() + maxMotionValue) / (tileWidth + tilesGap),
);

const nextActiveTileIdx = isPrev
? Math.max(0, numberOfHiddenTilesLeft - amountOfVisibleTiles)
: data!.length - numberOfHiddenTilesRight;

const { left: tileLeft } = tilesRef.current[nextActiveTileIdx].getBoundingClientRect()!;

const motionValue =
x.get() +
((isPrev ? tileLeft - tilesGap < containerLeft : tileLeft + tilesGap > containerLeft)
? -tileLeft + tilesGap + containerLeft
: 0);

const motionValueToSet = motionValue < -maxMotionValue ? -maxMotionValue : motionValue;

x.set(motionValueToSet);
};

useEffect(() => {
if (!constraintsRef.current || !videosWrapperRef.current || !data?.length) {
return;
}
const { width: containerInitialWidth } = constraintsRef.current!.getBoundingClientRect();
const { width: videosWrapperInitialWidth } = videosWrapperRef.current!.getBoundingClientRect();

setIsNextButtonActive(videosWrapperInitialWidth > containerInitialWidth);
}, [data?.length]);

const navigationArrowsProps = {
className: styles.arrows,
classNamePrevButton: styles.arrowLeft,
isNextButtonDisabled: !isNextButtonActive,
isPrevButtonDisabled: !isPrevButtonActive,
onClickNextButton: () => moveVideoBar(),
onClickPrevButton: () => moveVideoBar(true),
};

return (
<GridTile
className={className}
title={t('learnHowToUseOctant')}
titleSuffix={
<Button
className={styles.buttonClose}
Icon={<Svg img={cross} size={0.8} />}
onClick={() => setShowHelpVideos(false)}
variant="iconOnly"
/>
<>
{!isMobile && <NavigationArrows {...navigationArrowsProps} />}
<Button
className={styles.buttonClose}
Icon={<Svg img={cross} size={0.8} />}
onClick={() => setShowHelpVideos(false)}
variant="iconOnly"
/>
</>
}
>
<div ref={constraintsRef} className={styles.constraintsWrapper}>
<motion.div
ref={videosWrapperRef}
className={styles.videosWrapper}
drag="x"
dragConstraints={constraintsRef}
onDragEnd={() => setIsDragging(false)}
onDragStart={() => setIsDragging(true)}
style={{ x }}
>
{data?.map(({ name, player_embed_url }) => (
{data?.map(({ name, player_embed_url }, idx) => (
<VideoTile
key={player_embed_url}
ref={el => {
if (!el) {
return;
}
tilesRef.current[idx] = el;
return el;
}}
isDragging={isDragging}
title={name}
url={player_embed_url}
/>
))}
</motion.div>
</div>
{isMobile && <NavigationArrows {...navigationArrowsProps} />}
</GridTile>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Player from '@vimeo/player';
import cx from 'classnames';
import { AnimatePresence, motion, useInView } from 'framer-motion';
import React, { FC, Fragment, useEffect, useRef, useState } from 'react';
import React, { forwardRef, Fragment, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';

Expand All @@ -12,17 +12,17 @@ import { cross } from 'svg/misc';
import VideoTileProps from './types';
import styles from './VideoTile.module.scss';

const VideoTile: FC<VideoTileProps> = ({ title, url, isDragging }) => {
const VideoTile = ({ title, url, isDragging }, ref) => {
const { t } = useTranslation('translation', {
keyPrefix: 'components.home.homeGridVideoBar',
});
const ref = useRef<HTMLDivElement>(null);
const videoIframeRef = useRef<HTMLIFrameElement>(null);
const playerRef = useRef<Player>();
const previewVideoIframeRef = useRef<HTMLIFrameElement>(null);
const previewPlayerRef = useRef<Player>();

const isInView = useInView(ref, { amount: 'all' });
const isInViewRef = useRef<HTMLDivElement>(null);
const isInView = useInView(isInViewRef, { amount: 'all' });
const { isMobile, isTablet, isDesktop, isLargeDesktop } = useMediaQuery();

const urlWithOptions = `${url}&dnt=true`;
Expand Down Expand Up @@ -71,7 +71,17 @@ const VideoTile: FC<VideoTileProps> = ({ title, url, isDragging }) => {
}, [isMobile, isTablet, isDesktop, isLargeDesktop]);

return (
<div ref={ref} className={cx(styles.root, isInView && styles.isInView)}>
<div
ref={el => {
if (!el) {
return;
}
// @ts-expect-error wrong linter information that "current" is a read-only prop.
isInViewRef.current = el;
ref(el);
}}
className={cx(styles.root, isInView && styles.isInView)}
>
<div
className={styles.previewVideoOverlay}
onClick={() => {
Expand Down Expand Up @@ -161,4 +171,4 @@ const VideoTile: FC<VideoTileProps> = ({ title, url, isDragging }) => {
);
};

export default VideoTile;
export default forwardRef<HTMLDivElement, VideoTileProps>(VideoTile);
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,10 @@
}
}

.arrowsWrapper {
display: flex;
.arrows {
margin-left: auto;

.arrow {
cursor: pointer;
width: 3.2rem;
height: 3.2rem;
background: $color-octant-grey8;
border-radius: $border-radius-10;
transition: all $transition-time-5;
display: flex;
align-items: center;
justify-content: center;

&.leftArrow {
svg {
transform: rotate(180deg);
}
}

&.isDisabled {
background: $color-octant-grey6;

svg path {
fill: $color-octant-grey5;
}
}

&:not(.isDisabled):hover {
background: $color-octant-grey1;
}

&:first-child {
margin-right: 1.6rem;
}
.arrowLeft {
margin-right: 1.6rem;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import cx from 'classnames';
import React, { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import MetricsSectionHeader from 'components/Metrics/MetricsSectionHeader';
import Svg from 'components/ui/Svg';
import NavigationArrows from 'components/shared/NavigationArrows';
import useEpochDurationLabel from 'hooks/helpers/useEpochDurationLabel';
import useMediaQuery from 'hooks/helpers/useMediaQuery';
import useMetricsEpoch from 'hooks/helpers/useMetrcisEpoch';
import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen';
import { arrowRight } from 'svg/misc';

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

Expand All @@ -35,30 +33,14 @@ const MetricsEpochHeader = (): ReactElement => {
<div className={styles.epochDurationLabel}>{epochDurationLabel}</div>
)}
</div>
<div className={styles.arrowsWrapper}>
<div
className={cx(styles.arrow, styles.leftArrow, isLeftArrowDisabled && styles.isDisabled)}
onClick={() => {
if (isLeftArrowDisabled) {
return;
}
setEpoch(epoch - 1);
}}
>
<Svg img={arrowRight} size={1.4} />
</div>
<div
className={cx(styles.arrow, isRightArrowDisabled && styles.isDisabled)}
onClick={() => {
if (isRightArrowDisabled) {
return;
}
setEpoch(epoch + 1);
}}
>
<Svg img={arrowRight} size={1.4} />
</div>
</div>
<NavigationArrows
className={styles.arrows}
classNamePrevButton={styles.arrowLeft}
isNextButtonDisabled={isRightArrowDisabled}
isPrevButtonDisabled={isLeftArrowDisabled}
onClickNextButton={() => setEpoch(epoch + 1)}
onClickPrevButton={() => setEpoch(epoch - 1)}
/>
</MetricsSectionHeader>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.root {
display: flex;
align-items: center;

.arrow {
cursor: pointer;
width: 3.2rem;
height: 3.2rem;
background: $color-octant-grey6;
border-radius: $border-radius-10;
transition: all $transition-time-5;
display: flex;
align-items: center;
justify-content: center;

svg path {
fill: $color-octant-grey5;
}

&.leftArrow {
svg {
transform: rotate(180deg);
}
}

&.isDisabled {
background: $color-octant-grey6;

svg path {
fill: $color-octant-grey1;
}
}

&:not(.isDisabled):hover {
background: $color-octant-grey8;

svg path {
fill: $color-octant-dark;
}
}

&:not(.isDisabled):active {
background: $color-octant-grey1;

svg path {
fill: $color-octant-dark;
}
}
}
}
Loading

0 comments on commit 45a8ee9

Please sign in to comment.