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 && (
+
+ )}
-
- {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}
-
+
);
return (
- {link.image ? (
-
- ) : (
-
-
-
- )}
+
+ {previewImgState.loaded &&
+ (previewImgState.error ? (
+
+
+
+ ) : (
+
+ ))}
+
);
}
+
+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 && (
+
+ )}
{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 };