mirror of
https://github.com/filecoin-project/slate.git
synced 2024-11-30 02:12:38 +03:00
Merge pull request #865 from filecoin-project/@aminejv/preview-tweaks
Update: Object and Collection previews tweaks
This commit is contained in:
commit
0518a390ab
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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" />
|
||||
|
@ -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,16 +121,13 @@ 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>
|
||||
|
||||
{description && (
|
||||
<div ref={descriptionRef}>
|
||||
<P3
|
||||
style={{ paddingTop: 3, visibility: "hidden" }}
|
||||
@ -138,15 +137,18 @@ export default function CollectionPreview({ collection, viewer, owner, onAction
|
||||
{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 };
|
||||
|
@ -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_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;
|
||||
};
|
||||
|
@ -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,16 +147,13 @@ 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>
|
||||
|
||||
{description && (
|
||||
<div ref={descriptionRef}>
|
||||
<P3
|
||||
style={{ paddingTop: 3, visibility: "hidden" }}
|
||||
@ -169,12 +163,15 @@ export default function ObjectPreviewPrimitive({
|
||||
{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 };
|
||||
|
Loading…
Reference in New Issue
Block a user