Merge pull request #865 from filecoin-project/@aminejv/preview-tweaks

Update: Object and Collection previews tweaks
This commit is contained in:
martinalong 2021-08-16 15:59:09 -07:00 committed by GitHub
commit 0518a390ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 62 deletions

View File

@ -2,6 +2,7 @@ import * as React from "react";
import * as Logging from "~/common/logging";
import * as Actions from "~/common/actions";
import * as Events from "~/common/custom-events";
import * as Constants from "~/common/constants";
export const useMounted = (callback, depedencies) => {
const mountedRef = React.useRef(false);
@ -365,3 +366,27 @@ export function useMemoCompare(next, compare) {
*/
export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
export const useMediaQuery = () => {
const isMobileQuery = `(max-width: ${Constants.sizes.mobile}px)`;
const [isMobile, setMatch] = React.useState(true);
const handleResize = () => {
const isMobile = window.matchMedia(isMobileQuery).matches;
setMatch(isMobile);
};
React.useEffect(() => {
if (!window) return;
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// NOTE(amine): currently only support mobile breakpoint, we can add more breakpoints as needed.
return {
mobile: isMobile,
};
};

View File

@ -112,7 +112,7 @@ export const P3 = css`
font-family: ${Constants.font.text};
font-size: 0.75rem;
font-weight: normal;
line-height: 1.33;
line-height: 1.334;
letter-spacing: 0px;
${TEXT}

View File

@ -49,8 +49,7 @@ export const ExternalLink = (props) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
height={props.height}
style={props.style}
{...props}
>
<path d="M18 13V19C18 19.5304 17.7893 20.0391 17.4142 20.4142C17.0391 20.7893 16.5304 21 16 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V8C3 7.46957 3.21071 6.96086 3.58579 6.58579C3.96086 6.21071 4.46957 6 5 6H11" />
<path d="M15 3H21V9" />

View File

@ -11,7 +11,7 @@ import { motion, useAnimation } from "framer-motion";
import { Preview } from "~/components/core/CollectionPreviewBlock/components";
import { AspectRatio } from "~/components/system";
import { P3, H5, P2 } from "~/components/system/components/Typography";
import { useMounted } from "~/common/hooks";
import { useMediaQuery, useMounted } from "~/common/hooks";
const STYLES_CONTAINER = (theme) => css`
position: relative;
@ -83,8 +83,9 @@ export default function CollectionPreview({ collection, viewer, owner, onAction
const hideControls = () => setShowControls(false);
const description = collection?.data?.body;
const media = useMediaQuery();
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription({
disabled: !description,
disabled: !description || media.mobile,
});
const extendedDescriptionRef = React.useRef();
@ -99,6 +100,7 @@ export default function CollectionPreview({ collection, viewer, owner, onAction
const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer });
const { fileCount } = collection;
const title = collection?.data?.name || collection.slatename;
return (
<div css={STYLES_CONTAINER}>
@ -119,34 +121,34 @@ export default function CollectionPreview({ collection, viewer, owner, onAction
</motion.div>
</Preview>
<motion.article
css={STYLES_DESCRIPTION}
onMouseMove={showDescription}
onMouseLeave={hideDescription}
>
<motion.article css={STYLES_DESCRIPTION}>
<div style={{ position: "relative", paddingTop: 9 }}>
<H5 nbrOflines={1} style={{ visibility: "hidden" }}>
{collection.slatename}
{title}
</H5>
<div ref={descriptionRef}>
<P3
style={{ paddingTop: 3, visibility: "hidden" }}
nbrOflines={1}
color="textGrayDark"
>
{description}
</P3>
</div>
{description && (
<div ref={descriptionRef}>
<P3
style={{ paddingTop: 3, visibility: "hidden" }}
nbrOflines={1}
color="textGrayDark"
>
{description}
</P3>
</div>
)}
<motion.div
css={STYLES_INNER_DESCRIPTION}
initial={false}
animate={isDescriptionVisible ? "hovered" : "initial"}
variants={animationController.containerVariants}
onMouseMove={showDescription}
onMouseLeave={hideDescription}
>
<H5 color="textBlack" nbrOflines={1} title={collection.slatename}>
{collection.slatename}
<H5 color="textBlack" nbrOflines={1} title={title}>
{title}
</H5>
{!isDescriptionVisible && (
<P3 style={{ paddingTop: 3 }} nbrOflines={1} color="textGrayDark">
@ -265,7 +267,6 @@ const useAnimateDescription = ({
type: "spring",
stiffness: 170,
damping: 26,
delay: 0.3,
},
},
hovered: {
@ -281,11 +282,16 @@ const useAnimateDescription = ({
const descriptionControls = useAnimation();
useMounted(() => {
const extendedDescriptionElement = extendedDescriptionRef.current;
if (!extendedDescriptionElement) return;
if (isDescriptionVisible) {
extendedDescriptionElement.style.opacity = 1;
descriptionControls.start({ opacity: 1, transition: { delay: 0.2 } });
return;
}
descriptionControls.set({ opacity: 0 });
extendedDescriptionElement.style.opacity = 0;
}, [isDescriptionVisible]);
return { containerVariants, descriptionControls };

View File

@ -1,6 +1,7 @@
import * as React from "react";
import * as Styles from "~/common/styles";
import * as SVG from "~/common/svg";
import * as Constants from "~/common/constants";
import { P3 } from "~/components/system/components/Typography";
import { css } from "@emotion/react";
@ -8,6 +9,11 @@ import { css } from "@emotion/react";
import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive";
import LinkPlaceholder from "~/components/core/ObjectPreview/placeholders/Link";
const STYLES_CONTAINER = css`
${Styles.CONTAINER_CENTERED}
height: 100%;
`;
const STYLES_SOURCE_LOGO = css`
height: 12px;
width: 12px;
@ -15,19 +21,41 @@ const STYLES_SOURCE_LOGO = css`
`;
const STYLES_PLACEHOLDER_CONTAINER = css`
width: 100%;
height: 100%;
${Styles.CONTAINER_CENTERED}
`;
const STYLES_TAG_CONTAINER = (theme) => css`
color: ${theme.semantic.textGrayLight};
svg {
const STYLES_SOURCE = css`
transition: color 0.4s;
max-width: 80%;
`;
const STYLES_LINK = (theme) => css`
display: block;
width: 100%;
${Styles.LINK}
:hover small, .link_external_link {
color: ${theme.semantic.textGrayDark};
}
.link_external_link {
opacity: 0;
transition: opacity 0.3s;
}
:hover svg {
:hover .link_external_link {
opacity: 1;
}
`;
const STYLES_SMALL_IMG = css`
max-width: 100%;
height: auto;
object-fit: cover;
`;
const STYLES_TAG_CONTAINER = (theme) => css`
color: ${theme.semantic.textGray};
${Styles.HORIZONTAL_CONTAINER_CENTERED}
`;
@ -36,9 +64,15 @@ export default function LinkObjectPreview({ file, ratio, ...props }) {
data: { link },
} = file;
const previewImgState = useImage({
src: link.image,
maxWidth: Constants.grids.object.desktop.width,
});
const faviconImgState = useImage({ src: link.logo });
const tag = (
<a
css={Styles.LINK}
css={STYLES_LINK}
href={file.url}
target="_blank"
rel="noreferrer"
@ -46,7 +80,9 @@ export default function LinkObjectPreview({ file, ratio, ...props }) {
onClick={(e) => e.stopPropagation()}
>
<div css={STYLES_TAG_CONTAINER}>
{link.logo && (
{faviconImgState.error ? (
<SVG.Link height={12} width={12} style={{ marginRight: 4 }} />
) : (
<img
src={link.logo}
alt="Link source logo"
@ -54,23 +90,61 @@ export default function LinkObjectPreview({ file, ratio, ...props }) {
css={STYLES_SOURCE_LOGO}
/>
)}
<P3 as="small" color="textGray" nbrOflines={1}>
<P3 css={STYLES_SOURCE} as="small" color="textGray" nbrOflines={1}>
{link.source}
</P3>
<SVG.ExternalLink height={12} width={12} style={{ marginLeft: 4 }} />
<SVG.ExternalLink
className="link_external_link"
height={12}
width={12}
style={{ marginLeft: 4 }}
/>
</div>
</a>
);
return (
<ObjectPreviewPrimitive file={file} tag={tag} {...props}>
{link.image ? (
<img src={link.image} alt="Link preview" css={Styles.IMAGE_FILL} />
) : (
<div css={STYLES_PLACEHOLDER_CONTAINER}>
<LinkPlaceholder ratio={ratio} />
</div>
)}
<div css={STYLES_CONTAINER}>
{previewImgState.loaded &&
(previewImgState.error ? (
<div css={STYLES_PLACEHOLDER_CONTAINER}>
<LinkPlaceholder ratio={ratio} />
</div>
) : (
<img
src={link.image}
alt="Link preview"
css={previewImgState.overflow ? STYLES_SMALL_IMG : Styles.IMAGE_FILL}
/>
))}
</div>
</ObjectPreviewPrimitive>
);
}
const useImage = ({ src, maxWidth }) => {
const [imgState, setImgState] = React.useState({
loaded: false,
error: true,
overflow: false,
});
React.useEffect(() => {
if (!src) setImgState({ error: true, loaded: true });
const img = new Image();
img.src = src;
img.onload = () => {
if (maxWidth && img.naturalWidth < maxWidth) {
setImgState((prev) => ({ ...prev, loaded: true, error: false, overflow: true }));
} else {
setImgState({ loaded: true, error: false });
}
};
img.onerror = () => setImgState({ loaded: true, error: true });
}, []);
return imgState;
};

View File

@ -7,7 +7,7 @@ import { AspectRatio } from "~/components/system";
// import { LikeButton, SaveButton } from "./components";
// import { useSaveHandler } from "~/common/hooks";
import { motion, useAnimation } from "framer-motion";
import { useMounted } from "~/common/hooks";
import { useMounted, useMediaQuery } from "~/common/hooks";
import ImageObjectPreview from "./ImageObjectPreview";
@ -28,10 +28,6 @@ const STYLES_DESCRIPTION = (theme) => css`
width: 100%;
background-color: ${theme.semantic.bgLight};
z-index: 1;
@media (max-width: ${theme.sizes.mobile}px) {
padding: 8px;
}
`;
const STYLES_INNER_DESCRIPTION = (theme) => css`
@ -95,8 +91,9 @@ export default function ObjectPreviewPrimitive({
// const hideControls = () => setShowControls(false);
const description = file?.data?.body;
const media = useMediaQuery();
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription({
disabled: !description,
disabled: !description || media.mobile,
});
const extendedDescriptionRef = React.useRef();
@ -150,31 +147,31 @@ export default function ObjectPreviewPrimitive({
{children}
</div>
<motion.article
css={STYLES_DESCRIPTION}
onMouseMove={showDescription}
onMouseLeave={hideDescription}
>
<motion.article css={STYLES_DESCRIPTION}>
<div style={{ position: "relative", paddingTop: 9 }}>
<H5 as="h2" nbrOflines={1} style={{ visibility: "hidden" }}>
{title}
</H5>
<div ref={descriptionRef}>
<P3
style={{ paddingTop: 3, visibility: "hidden" }}
nbrOflines={1}
color="textGrayDark"
>
{description}
</P3>
</div>
{description && (
<div ref={descriptionRef}>
<P3
style={{ paddingTop: 3, visibility: "hidden" }}
nbrOflines={1}
color="textGrayDark"
>
{description}
</P3>
</div>
)}
<motion.div
css={STYLES_INNER_DESCRIPTION}
initial={false}
animate={isDescriptionVisible ? "hovered" : "initial"}
variants={animationController.containerVariants}
onMouseMove={showDescription}
onMouseLeave={hideDescription}
>
<H5 as="h2" nbrOflines={1} color="textBlack" title={title}>
{title}
@ -229,7 +226,7 @@ const useShowDescription = ({ disabled }) => {
if (disabled) return;
clearTimeout(timeoutId.current);
const id = setTimeout(() => setShowDescription(true), 250);
const id = setTimeout(() => setShowDescription(true), 200);
timeoutId.current = id;
};
const hideDescription = () => {
@ -269,7 +266,6 @@ const useAnimateDescription = ({
type: "spring",
stiffness: 170,
damping: 26,
delay: 0.3,
},
},
hovered: {
@ -285,11 +281,16 @@ const useAnimateDescription = ({
const descriptionControls = useAnimation();
useMounted(() => {
const extendedDescriptionElement = extendedDescriptionRef.current;
if (!extendedDescriptionElement) return;
if (isDescriptionVisible) {
extendedDescriptionElement.style.opacity = 1;
descriptionControls.start({ opacity: 1, transition: { delay: 0.2 } });
return;
}
descriptionControls.set({ opacity: 0 });
extendedDescriptionElement.style.opacity = 0;
}, [isDescriptionVisible]);
return { containerVariants, descriptionControls };