diff --git a/common/hooks.js b/common/hooks.js index d692d7f0..20cece22 100644 --- a/common/hooks.js +++ b/common/hooks.js @@ -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, + }; +}; diff --git a/common/styles.js b/common/styles.js index 3d1a55a6..c9a2fb67 100644 --- a/common/styles.js +++ b/common/styles.js @@ -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} diff --git a/common/svg.js b/common/svg.js index 38352112..21b67403 100644 --- a/common/svg.js +++ b/common/svg.js @@ -49,8 +49,7 @@ export const ExternalLink = (props) => { strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - height={props.height} - style={props.style} + {...props} > diff --git a/components/core/CollectionPreviewBlock/index.js b/components/core/CollectionPreviewBlock/index.js index b04737e9..5b31c9bf 100644 --- a/components/core/CollectionPreviewBlock/index.js +++ b/components/core/CollectionPreviewBlock/index.js @@ -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 (
@@ -119,34 +121,34 @@ export default function CollectionPreview({ collection, viewer, owner, onAction - +
- {collection.slatename} + {title}
-
- - {description} - -
+ {description && ( +
+ + {description} + +
+ )} -
- {collection.slatename} +
+ {title}
{!isDescriptionVisible && ( @@ -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 }; diff --git a/components/core/ObjectPreview/LinkObjectPreview.js b/components/core/ObjectPreview/LinkObjectPreview.js index 1394053d..756541f8 100644 --- a/components/core/ObjectPreview/LinkObjectPreview.js +++ b/components/core/ObjectPreview/LinkObjectPreview.js @@ -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 = ( e.stopPropagation()} >
- {link.logo && ( + {faviconImgState.error ? ( + + ) : ( Link source logo )} - + {link.source} - +
); return ( - {link.image ? ( - Link preview - ) : ( -
- -
- )} +
+ {previewImgState.loaded && + (previewImgState.error ? ( +
+ +
+ ) : ( + Link preview + ))} +
); } + +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; +}; diff --git a/components/core/ObjectPreview/ObjectPreviewPrimitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js index 554279c6..9a90943a 100644 --- a/components/core/ObjectPreview/ObjectPreviewPrimitive.js +++ b/components/core/ObjectPreview/ObjectPreviewPrimitive.js @@ -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}
- +
{title}
-
- - {description} - -
+ {description && ( +
+ + {description} + +
+ )}
{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 };