Browse Source

Carousel Refactor And Minor Refactors

Francesco Baccetti 4 years ago
parent
commit
8895909007

+ 1 - 2
packages/app/src/components/ChannelGallery.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import { css } from "@emotion/core";
-import Gallery from "./Gallery";
-import { ChannelPreview } from "@joystream/components";
+import { ChannelPreview, Gallery } from "@joystream/components";
 
 const channels = [
 	{

+ 0 - 43
packages/app/src/components/Gallery.tsx

@@ -1,43 +0,0 @@
-import React from "react";
-import { css } from "@emotion/core";
-
-import { Carousel, Button, theme } from "@joystream/components";
-
-type GalleryProps = {
-	title: string;
-	children: React.ReactNode;
-	onSeeAll: () => void;
-	seeAll: boolean;
-};
-
-const styles = {
-	section: css`
-		margin-bottom: 2rem;
-		padding: 1rem;
-	`,
-	headingContainer: css`
-		display: flex;
-		justify-content: space-between;
-		align-items: baseline;
-		& > h4 {
-			font-size: ${theme.typography.sizes.h4};
-			margin-block: 1rem;
-		}
-	`,
-};
-
-export default function Gallery({ title = "", children, seeAll = false, onSeeAll }: Partial<GalleryProps>) {
-	return (
-		<section css={styles.section}>
-			<div css={styles.headingContainer}>
-				<h4>{title}</h4>
-				{seeAll && (
-					<Button type="tertiary" onClick={onSeeAll}>
-						See All
-					</Button>
-				)}
-			</div>
-			<Carousel>{children}</Carousel>
-		</section>
-	);
-}

+ 1 - 2
packages/app/src/components/SeriesGallery.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import { css } from "@emotion/core";
-import Gallery from "./Gallery";
-import { SeriesPreview } from "@joystream/components";
+import { Gallery, SeriesPreview } from "@joystream/components";
 
 type SeriesGalleryProps = {
 	title?: string;

+ 1 - 2
packages/app/src/components/Tags.tsx

@@ -1,6 +1,5 @@
 import React from "react";
-import Gallery from "./Gallery";
-import { TagButton } from "@joystream/components";
+import { TagButton, Gallery } from "@joystream/components";
 
 const tags = [
 	"finance",

+ 14 - 6
packages/app/src/components/VideoGallery.tsx

@@ -1,8 +1,7 @@
-import React from "react";
-import { css } from "@emotion/core";
+import React, { useCallback, useEffect, useState } from "react";
+import { css, SerializedStyles } from "@emotion/core";
 
-import { VideoPreview, theme } from "@joystream/components";
-import Gallery from "./Gallery";
+import { VideoPreview, Gallery, theme } from "@joystream/components";
 
 type VideoGalleryProps = {
 	title: string;
@@ -98,10 +97,18 @@ const articleStyles = css`
 	margin: auto ${theme.spacing.m};
 `;
 
-export default function VideoGallery({ title = "", log = false }: VideoGalleryProps) {
+export default function VideoGallery({ title = "" }: VideoGalleryProps) {
 	const videos = videoPlaceholders.concat(videoPlaceholders).concat(videoPlaceholders);
+	const [controlsTop, setControlsTop] = useState<SerializedStyles>(css``);
+	const imgRef = useCallback((node: HTMLImageElement) => {
+		if (node != null) {
+			setControlsTop(css`
+				top: calc(${Math.round(node.clientHeight) / 2}px - 24px);
+			`);
+		}
+	}, []);
 	return (
-		<Gallery title={title} seeAll>
+		<Gallery title={title} action="See All" leftControlCss={controlsTop} rightControlCss={controlsTop}>
 			{videos.map((video, idx) => (
 				<article css={articleStyles} key={`${title}- ${video.title} - ${idx}`}>
 					<VideoPreview
@@ -110,6 +117,7 @@ export default function VideoGallery({ title = "", log = false }: VideoGalleryPr
 						showChannel={video.showChannel}
 						views={video.views}
 						time={video.time}
+						imgRef={idx === 0 ? imgRef : null}
 						poster={video.poster}
 					/>
 				</article>

+ 2 - 1
packages/components/package.json

@@ -33,7 +33,8 @@
 	},
 	"dependencies": {
 		"react-player": "^2.2.0",
-		"react-spring": "^8.0.27"
+		"react-spring": "^8.0.27",
+		"use-resize-observer": "^6.1.0"
 	},
 	"peerDependencies": {
 		"@babel/runtime": "^7.10.2",

+ 1 - 1
packages/components/src/components/Avatar/Avatar.tsx

@@ -3,7 +3,7 @@ import { SerializedStyles } from "@emotion/core";
 import { makeStyles, AvatarStyleProps } from "./Avatar.style";
 
 export type AvatarProps = {
-	onClick?: (e: React.MouseEvent) => void;
+	onClick?: (e: React.MouseEvent<HTMLElement>) => void;
 	outerStyles?: SerializedStyles;
 } & AvatarStyleProps;
 

+ 23 - 24
packages/components/src/components/Carousel/Carousel.style.ts

@@ -1,42 +1,41 @@
 import { StyleFn, makeStyles } from "../../utils"
-import { spacing } from "../../theme"
-export type CarouselStyleProps = {
-	navTopPosition?: string
-}
+import { spacing, breakpoints } from "../../theme"
+
+export type CarouselStyleProps = {}
 
 const container: StyleFn = () => ({
 	position: "relative",
-	display: "flex",
-	alignItems: "center"
+	display: "flex"
 })
-const innerContainer: StyleFn = () => ({
+const itemsContainer: StyleFn = () => ({
 	display: "flex",
 	overflow: "hidden",
-	padding: "1rem"
+	padding: "1rem 0"
 })
 
-const navLeft: StyleFn = () => ({
-	order: -1,
-	position: "relative",
-	zIndex: 1,
-	left: 48,
+const navBase: StyleFn = () => ({
 	minWidth: spacing.xxxxl,
 	minHeight: spacing.xxxxl,
-	marginTop: -80
+	width: spacing.xxxxl,
+	height: spacing.xxxxl,
+	position: "absolute"
 })
 
-const navRight: StyleFn = () => ({
-	position: "relative",
-	zIndex: 1,
-	right: 48,
-	minWidth: spacing.xxxxl,
-	minHeight: spacing.xxxxl,
-	marginTop: -80
+const navLeft: StyleFn = styles => ({
+	...styles,
+	left: 0,
+	top: `calc(50% - ${Math.round((parseInt(spacing.xxxxl) + 1) / 2)}px)`
+})
+
+const navRight: StyleFn = styles => ({
+	...styles,
+	right: 0,
+	top: `calc(50% - ${Math.round((parseInt(spacing.xxxxl) + 1) / 2)}px)`
 })
 
 export const useCSS = (props: CarouselStyleProps) => ({
 	container: makeStyles([container])(props),
-	innerContainer: makeStyles([innerContainer])(props),
-	navLeft: makeStyles([navLeft])(props),
-	navRight: makeStyles([navRight])(props)
+	itemsContainer: makeStyles([itemsContainer])(props),
+	navLeft: makeStyles([navBase, navLeft])(props),
+	navRight: makeStyles([navBase, navRight])(props)
 })

+ 74 - 94
packages/components/src/components/Carousel/Carousel.tsx

@@ -1,125 +1,105 @@
-import React, { useState, useRef, useEffect, useCallback } from "react";
-import { css } from "@emotion/core";
+import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
+import { css, SerializedStyles } from "@emotion/core";
 import { animated, useSpring } from "react-spring";
+import useResizeObserver from "use-resize-observer";
 import { useCSS, CarouselStyleProps } from "./Carousel.style";
 import NavButton from "../NavButton";
 
 type CarouselProps = {
-	children: React.ReactNode[];
-	scrollAmount?: number;
-	log?: boolean;
+	children: React.ReactNode;
+	containerCss: SerializedStyles;
+	leftControlCss: SerializedStyles;
+	rightControlCss: SerializedStyles;
+	onScroll: (direction: "left" | "right") => void;
 } & CarouselStyleProps;
 
-export default function Carousel({ children, scrollAmount = 200, log, ...styleProps }: CarouselProps) {
-	let styles = useCSS(styleProps);
-	const containerRef = useRef<HTMLDivElement>(null);
-	const elementsRefs = useRef<(HTMLDivElement | null)[]>([]);
-	const [distance, setDistance] = useState(0);
-	const [maxDistance, setMaxDistance] = useState(Infinity);
-	const [props, set] = useSpring(() => ({
-		transform: `translateX(${distance}px)`,
+const Carousel: React.FC<Partial<CarouselProps>> = ({
+	children,
+	containerCss,
+	leftControlCss,
+	rightControlCss,
+	onScroll = () => {},
+}) => {
+	if (!Array.isArray(children)) {
+		return <>{children}</>;
+	}
+	let [props, set] = useSpring(() => ({
+		transform: `translateX(0px)`,
 	}));
-
+	const [x, setX] = useState(0);
+	const { width: containerWidth, ref: containerRef } = useResizeObserver<HTMLDivElement>();
+	const elementsRefs = useRef<(HTMLDivElement | null)[]>([]);
+	const [childrenLength, setChildrenLength] = useState(0);
 	useEffect(() => {
-		if (containerRef.current) {
-			elementsRefs.current = elementsRefs.current.slice(0, children.length);
-			const totalChildrensLength = elementsRefs.current.reduce(
-				(accWidth, el) => (el != null ? accWidth + el.clientWidth : accWidth),
-				0
-			);
-			const longestChildrenWidth = elementsRefs.current.reduce(
-				(longest, el) => (el != null && el.clientWidth > longest ? el.clientWidth : longest),
-				0
-			);
-			const containerWidth = containerRef.current.clientWidth;
-
-			setMaxDistance(totalChildrensLength - containerWidth + longestChildrenWidth);
-		}
+		elementsRefs.current = elementsRefs.current.slice(0, children.length);
+		const childrensLength = elementsRefs.current.reduce(
+			(accWidth, el) => (el != null ? accWidth + el.clientWidth : accWidth),
+			0
+		);
+		setChildrenLength(childrensLength);
 	}, [children.length]);
 
-	if (log) {
-		console.log({
-			totalChildrensLength: elementsRefs.current.reduce(
-				(accWidth, el) => (el != null ? accWidth + el.clientWidth : accWidth),
-				0
-			),
-			longestChildrenWidth: elementsRefs.current.reduce(
-				(longest, el) => (el != null && el.clientWidth > longest ? el.clientWidth : longest),
-				0
-			),
-			maxDistance,
-			childrens: children.length,
-			distance,
-		});
-	}
-	const MIN_DISTANCE = 0;
-	const MAX_DISTANCE = maxDistance;
-
-	function handleScroll(direction: "right" | "left") {
-		let newDist = NaN;
-
-		switch (direction) {
-			case "left": {
-				newDist = distance + scrollAmount <= MIN_DISTANCE ? distance + scrollAmount : distance;
-				break;
-			}
-			case "right": {
-				newDist = distance - scrollAmount > -MAX_DISTANCE ? distance - scrollAmount : distance;
-				break;
-			}
-		}
-		console.log("newDist", newDist);
-		setDistance(newDist);
-		set({
-			transform: `translateX(${newDist}px)`,
-		});
-
-		return newDist;
-	}
-
+	const styles = useMemo(() => useCSS({}), []);
 	return (
-		<div css={styles.container}>
-			<div css={styles.innerContainer} ref={containerRef}>
-				{children.map((item, idx) => (
+		<div css={[styles.container, containerCss]}>
+			<div css={styles.itemsContainer} ref={containerRef}>
+				{children.map((element, idx) => (
 					<animated.div
 						style={props}
 						key={`Carousel-${idx}`}
-						css={css`
-							&::after {
-								background-color: red;
-							}
-						`}
 						ref={(el) => {
 							elementsRefs.current[idx] = el;
 							return el;
 						}}
 					>
-						{item}
+						{element}
 					</animated.div>
 				))}
 			</div>
 			<NavButton
-				outerCss={[
-					styles.navLeft,
-					css`
-						opacity: ${distance === MIN_DISTANCE ? 0 : 1};
-					`,
-				]}
-				type="primary"
+				outerCss={[styles.navLeft, leftControlCss]}
 				direction="left"
-				onClick={() => handleScroll("left")}
+				onClick={() => {
+					handleScroll("left");
+				}}
 			/>
 			<NavButton
-				outerCss={[
-					styles.navRight,
-					css`
-						opacity: ${distance - scrollAmount < -MAX_DISTANCE ? 0 : 1};
-					`,
-				]}
-				type="primary"
+				outerCss={[styles.navRight, rightControlCss]}
 				direction="right"
-				onClick={() => handleScroll("right")}
+				onClick={() => {
+					handleScroll("right");
+				}}
 			/>
 		</div>
 	);
-}
+
+	function handleScroll(direction: "left" | "right") {
+		if (containerWidth == null) {
+			return;
+		}
+		let scrollAmount;
+		switch (direction) {
+			case "left": {
+				// Prevent overscroll on the left
+				scrollAmount = x + containerWidth >= 0 ? 0 : x + containerWidth;
+				onScroll("left");
+				break;
+			}
+			case "right": {
+				// Prevent overscroll on the right
+				scrollAmount =
+					x - containerWidth <= -(childrenLength - containerWidth)
+						? -(childrenLength - containerWidth)
+						: x - containerWidth;
+				onScroll("right");
+				break;
+			}
+		}
+		setX(scrollAmount);
+		set({
+			transform: `translateX(${scrollAmount}px)`,
+		});
+	}
+};
+
+export default Carousel;

+ 24 - 0
packages/components/src/components/Gallery/Gallery.style.ts

@@ -0,0 +1,24 @@
+import { colors, spacing, typography } from "../../theme"
+import { makeStyles, StyleFn } from "../../utils"
+
+const container: StyleFn = () => ({
+	marginBottom: spacing.xxl,
+	padding: spacing.m,
+	display: "flex",
+	flexDirection: "column"
+})
+
+const headingContainer: StyleFn = () => ({
+	display: "flex",
+	justifyContent: "space-between",
+	alignItems: "baseline",
+	"& > h4": {
+		fontSize: typography.sizes.h4,
+		marginBlock: spacing.m
+	}
+})
+
+export const useCSS = () => ({
+	container: makeStyles([container])({}),
+	headingContainer: makeStyles([headingContainer])({})
+})

+ 44 - 0
packages/components/src/components/Gallery/Gallery.tsx

@@ -0,0 +1,44 @@
+import React from "react";
+import { SerializedStyles } from "@emotion/core";
+import { useCSS } from "./Gallery.style";
+import Button from "../Button";
+import Carousel from "../Carousel";
+
+type GalleryProps = {
+	title: string;
+	action: string;
+	onClick: () => void;
+	children: React.ReactNode[];
+	containerCss: SerializedStyles;
+	leftControlCss: SerializedStyles;
+	rightControlCss: SerializedStyles;
+};
+
+const Gallery: React.FC<Partial<GalleryProps>> = ({
+	title,
+	onClick,
+	action = "",
+	children,
+	containerCss,
+	leftControlCss,
+	rightControlCss,
+}) => {
+	const styles = useCSS();
+	return (
+		<section css={[styles.container, containerCss]}>
+			<div css={styles.headingContainer}>
+				{title && <h4>{title}</h4>}
+				{action && (
+					<Button type="tertiary" onClick={onClick}>
+						{action}
+					</Button>
+				)}
+			</div>
+			<Carousel leftControlCss={leftControlCss} rightControlCss={rightControlCss}>
+				{children}
+			</Carousel>
+		</section>
+	);
+};
+
+export default Gallery;

+ 2 - 0
packages/components/src/components/Gallery/index.ts

@@ -0,0 +1,2 @@
+import Gallery from "./Gallery"
+export default Gallery

+ 1 - 1
packages/components/src/components/NavButton/NavButton.tsx

@@ -6,7 +6,7 @@ import ChevronLeftIcon from "../../../assets/chevron-left-big.svg";
 
 type NavButtonProps = {
 	direction: "right" | "left";
-	outerCss: SerializedStyles | SerializedStyles[];
+	outerCss: SerializedStyles | SerializedStyles[] | (SerializedStyles | undefined) | (SerializedStyles | undefined)[];
 	onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
 } & NavButtonStyleProps;
 

+ 10 - 10
packages/components/src/components/VideoPreview/VideoPreview.styles.tsx

@@ -2,11 +2,12 @@ import { css } from "@emotion/core";
 import { typography, colors } from "./../../theme";
 
 export type VideoPreviewStyleProps = {
-	showChannel?: boolean;
-	poster?: string;
-	width?: number;
-	darken?: boolean;
-	height?: number;
+	showChannel: boolean;
+	poster: string;
+	width: number;
+	darken: boolean;
+	height: number;
+	fade: string;
 };
 
 export let makeStyles = ({
@@ -14,9 +15,9 @@ export let makeStyles = ({
 	width = 320,
 	height = 190,
 	poster = "",
-	darken = false,
-}: VideoPreviewStyleProps) => {
-	const withPoster = poster ? `url(${poster})` : `linear-gradient(${colors.gray[300]}, ${colors.gray[700]})`;
+	fade,
+}: Partial<VideoPreviewStyleProps>) => {
+	const withPoster = poster ? fade : `linear-gradient(${colors.gray[300]}, ${colors.gray[700]})`;
 
 	return {
 		container: css`
@@ -28,13 +29,12 @@ export let makeStyles = ({
 		coverContainer: css`
 			width: ${width}px;
 			height: ${height}px;
-			background-color: black;
 		`,
 		cover: css`
 			width: 100%;
 			height: 100%;
 			background-image: ${withPoster};
-			background-size: cover;
+			object-fit: cover;
 		`,
 		infoContainer: css`
 			display: grid;

+ 22 - 40
packages/components/src/components/VideoPreview/VideoPreview.tsx

@@ -1,57 +1,44 @@
 import React from "react";
-
 import { makeStyles, VideoPreviewStyleProps } from "./VideoPreview.styles";
 import Avatar from "./../Avatar";
 
 type VideoPreviewProps = {
 	title: string;
-	channel?: string;
-	channelImg?: string;
-	showChannel?: boolean;
-	showMeta?: boolean;
-	time?: string;
-	views?: string;
-	poster?: string;
-	onClick?: any;
-	onChannelClick?: any;
+	channel: string;
+	channelImg: string;
+	showChannel: boolean;
+	showMeta: boolean;
+	time: string;
+	views: string;
+	poster: string;
+	onClick: any;
+	imgRef: React.Ref<HTMLImageElement>;
+	onChannelClick: (e: React.MouseEvent<HTMLElement>) => void;
 } & VideoPreviewStyleProps;
 
-export default function VideoPreview({
+const VideoPreview: React.FC<Partial<VideoPreviewProps>> = ({
 	title,
 	channel,
 	channelImg,
-	showChannel = true,
-	showMeta = true,
+	showChannel,
+	showMeta,
 	time,
 	views,
+	imgRef,
 	poster,
-	onClick,
-	onChannelClick,
+	onClick = () => {},
+	onChannelClick = () => {},
 	...styleProps
-}: VideoPreviewProps) {
+}) => {
 	let styles = makeStyles({ showChannel, poster, ...styleProps });
 	return (
 		<div css={styles.container} onClick={onClick}>
 			<div css={styles.coverContainer}>
-				<div
-					css={styles.cover}
-					onClick={(event) => {
-						event.stopPropagation();
-						onClick();
-					}}
-				/>
+				<img src={poster} ref={imgRef} css={styles.cover} alt={`${title} by ${title} thumbnail`} />
 			</div>
 			<div css={styles.infoContainer}>
 				{showChannel && (
-					<Avatar
-						size="small"
-						img={channelImg}
-						outerStyles={styles.avatar}
-						onClick={(event) => {
-							event.stopPropagation();
-							onChannelClick();
-						}}
-					/>
+					<Avatar size="small" img={channelImg} outerStyles={styles.avatar} onClick={onChannelClick} />
 				)}
 				<div css={styles.textContainer}>
 					<h3
@@ -64,13 +51,7 @@ export default function VideoPreview({
 						{title}
 					</h3>
 					{showChannel && (
-						<span
-							css={styles.channel}
-							onClick={(event) => {
-								event.stopPropagation();
-								onChannelClick();
-							}}
-						>
+						<span css={styles.channel} onClick={onChannelClick}>
 							{channel}
 						</span>
 					)}
@@ -83,4 +64,5 @@ export default function VideoPreview({
 			</div>
 		</div>
 	);
-}
+};
+export default VideoPreview;

+ 1 - 0
packages/components/src/index.ts

@@ -19,4 +19,5 @@ export { default as VideoPreview } from "./components/VideoPreview"
 export { default as VideoPlayer } from "./components/VideoPlayer"
 export { default as SeriesPreview } from "./components/SeriesPreview"
 export { default as ChannelPreview } from "./components/ChannelPreview"
+export { default as Gallery } from "./components/Gallery"
 export { theme }

+ 7 - 0
yarn.lock

@@ -18489,6 +18489,13 @@ use-callback-ref@^1.2.1:
   resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.4.tgz#d86d1577bfd0b955b6e04aaf5971025f406bea3c"
   integrity sha512-rXpsyvOnqdScyied4Uglsp14qzag1JIemLeTWGKbwpotWht57hbP78aNT+Q4wdFKQfQibbUX4fb6Qb4y11aVOQ==
 
+use-resize-observer@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-6.1.0.tgz#d4d267a940dbf9c326da6042f8a4bb8c89d29729"
+  integrity sha512-SiPcWHiIQ1CnHmb6PxbYtygqiZXR0U9dNkkbpX9VYnlstUwF8+QqpUTrzh13pjPwcjMVGR+QIC+nvF5ujfFNng==
+  dependencies:
+    resize-observer-polyfill "^1.5.1"
+
 use-sidecar@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"