mirror of
https://github.com/filecoin-project/slate.git
synced 2024-12-25 10:05:02 +03:00
Merge pull request #864 from filecoin-project/@aminejv/preview-tweaks
Update: Object and Collection previews tweaks
This commit is contained in:
commit
d9c355cabe
@ -127,6 +127,7 @@ export const shadow = {
|
|||||||
darkSmall: "0px 4px 16px 0 rgba(99, 101, 102, 0.1)",
|
darkSmall: "0px 4px 16px 0 rgba(99, 101, 102, 0.1)",
|
||||||
darkMedium: "0px 8px 32px 0 rgba(99, 101, 102, 0.2)",
|
darkMedium: "0px 8px 32px 0 rgba(99, 101, 102, 0.2)",
|
||||||
darkLarge: "0px 12px 64px 0 rgba(99, 101, 102, 0.3)",
|
darkLarge: "0px 12px 64px 0 rgba(99, 101, 102, 0.3)",
|
||||||
|
card: "0px 0px 32px #E5E8EA;",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zindex = {
|
export const zindex = {
|
||||||
@ -204,12 +205,12 @@ export const grids = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
object: {
|
object: {
|
||||||
desktop: { width: 248, rowGap: 16 },
|
desktop: { width: 248, rowGap: 20 },
|
||||||
mobile: { width: 166, rowGap: 8 },
|
mobile: { width: 166, rowGap: 12 },
|
||||||
},
|
},
|
||||||
collection: {
|
collection: {
|
||||||
desktop: { width: 432, rowGap: 16 },
|
desktop: { width: 382, rowGap: 16 },
|
||||||
mobile: { width: 300, rowGap: 8 },
|
mobile: { width: 280, rowGap: 8 },
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
desktop: { width: 248, rowGap: 16 },
|
desktop: { width: 248, rowGap: 16 },
|
||||||
|
@ -5,7 +5,7 @@ import * as Events from "~/common/custom-events";
|
|||||||
|
|
||||||
export const useMounted = (callback, depedencies) => {
|
export const useMounted = (callback, depedencies) => {
|
||||||
const mountedRef = React.useRef(false);
|
const mountedRef = React.useRef(false);
|
||||||
React.useLayoutEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
if (mountedRef.current && callback) {
|
if (mountedRef.current && callback) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@ -230,7 +230,7 @@ export const useIntersection = ({ onIntersect, ref }, dependencies = []) => {
|
|||||||
const onIntersectRef = React.useRef();
|
const onIntersectRef = React.useRef();
|
||||||
onIntersectRef.current = onIntersect;
|
onIntersectRef.current = onIntersect;
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const lazyObserver = new IntersectionObserver((entries) => {
|
const lazyObserver = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
@ -404,3 +404,10 @@ export function useMemoCompare(next, compare) {
|
|||||||
|
|
||||||
return isEqual ? previous : next;
|
return isEqual ? previous : next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE(amine): use this hook to get rid of nextJs warnings
|
||||||
|
* source: https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a
|
||||||
|
*/
|
||||||
|
export const useIsomorphicLayoutEffect =
|
||||||
|
typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
|
||||||
|
123
components/core/CollectionPreviewBlock/components/Preview.js
Normal file
123
components/core/CollectionPreviewBlock/components/Preview.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as Validations from "~/common/validations";
|
||||||
|
import * as Typography from "~/components/system/components/Typography";
|
||||||
|
import * as Strings from "~/common/strings";
|
||||||
|
import * as Constants from "~/common/constants";
|
||||||
|
|
||||||
|
import { Logo } from "~/common/logo";
|
||||||
|
import { useInView } from "~/common/hooks";
|
||||||
|
import { isBlurhashValid } from "blurhash";
|
||||||
|
import { Blurhash } from "react-blurhash";
|
||||||
|
import { css } from "@emotion/react";
|
||||||
|
|
||||||
|
import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders";
|
||||||
|
|
||||||
|
const STYLES_PLACEHOLDER_CONTAINER = css`
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_PREVIEW = css`
|
||||||
|
flex-grow: 1;
|
||||||
|
background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_EMPTY_CONTAINER = css`
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function Preview({ collection, children, ...props }) {
|
||||||
|
const [isLoading, setLoading] = React.useState(true);
|
||||||
|
const handleOnLoaded = () => setLoading(false);
|
||||||
|
|
||||||
|
const previewerRef = React.useRef();
|
||||||
|
const { isInView } = useInView({
|
||||||
|
ref: previewerRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
const object = React.useMemo(() => getObjectToPreview(collection.objects), [collection.objects]);
|
||||||
|
|
||||||
|
const isCollectionEmpty = collection.objects.length === 0;
|
||||||
|
if (isCollectionEmpty) {
|
||||||
|
return (
|
||||||
|
<div css={STYLES_EMPTY_CONTAINER} {...props}>
|
||||||
|
{children}
|
||||||
|
<Logo style={{ height: 24, marginBottom: 8, color: Constants.system.grayLight2 }} />
|
||||||
|
<Typography.P2 color="grayLight2">This collection is empty</Typography.P2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object.isImage) {
|
||||||
|
const { coverImage } = object.data;
|
||||||
|
const blurhash = getFileBlurHash(object);
|
||||||
|
const previewImage = coverImage
|
||||||
|
? Strings.getURLfromCID(coverImage?.cid)
|
||||||
|
: Strings.getURLfromCID(object.cid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={previewerRef} css={STYLES_PREVIEW} {...props}>
|
||||||
|
{children}
|
||||||
|
{isInView && (
|
||||||
|
<>
|
||||||
|
{isLoading && blurhash && (
|
||||||
|
<Blurhash
|
||||||
|
hash={blurhash}
|
||||||
|
style={{ position: "absolute", top: 0, left: 0 }}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
resolutionX={32}
|
||||||
|
resolutionY={32}
|
||||||
|
punch={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<img src={previewImage} alt="Collection preview" onLoad={handleOnLoaded} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div css={STYLES_PREVIEW} {...props}>
|
||||||
|
{children}
|
||||||
|
<ObjectPlaceholder ratio={1} containerCss={STYLES_PLACEHOLDER_CONTAINER} file={object} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileBlurHash = (file) => {
|
||||||
|
const coverImage = file?.data?.coverImage;
|
||||||
|
const coverImageBlurHash = coverImage?.data?.blurhash;
|
||||||
|
if (coverImage && isBlurhashValid(coverImageBlurHash)) return coverImageBlurHash;
|
||||||
|
|
||||||
|
const blurhash = file?.data?.blurhash;
|
||||||
|
if (isBlurhashValid(blurhash)) return blurhash;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getObjectToPreview = (objects = []) => {
|
||||||
|
let objectIdx = 0;
|
||||||
|
let isImage = false;
|
||||||
|
|
||||||
|
objects.some((object, i) => {
|
||||||
|
const isPreviewableImage = Validations.isPreviewableImage(object.data.type);
|
||||||
|
if (isPreviewableImage) (objectIdx = i), (isImage = true);
|
||||||
|
return isPreviewableImage;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...objects[objectIdx], isImage };
|
||||||
|
};
|
@ -1 +1,2 @@
|
|||||||
export { default as FollowButton } from "./FollowButton";
|
export { default as FollowButton } from "./FollowButton";
|
||||||
|
export { default as Preview } from "./Preview";
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Validations from "~/common/validations";
|
|
||||||
import * as Typography from "~/components/system/components/Typography";
|
|
||||||
import * as Styles from "~/common/styles";
|
import * as Styles from "~/common/styles";
|
||||||
import * as Strings from "~/common/strings";
|
|
||||||
import * as Constants from "~/common/constants";
|
import * as Constants from "~/common/constants";
|
||||||
import * as SVG from "~/common/svg";
|
import * as SVG from "~/common/svg";
|
||||||
|
|
||||||
import { Logo } from "~/common/logo";
|
|
||||||
import { useInView } from "~/common/hooks";
|
|
||||||
import { isBlurhashValid } from "blurhash";
|
|
||||||
import { Blurhash } from "react-blurhash";
|
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
import { FollowButton } from "~/components/core/CollectionPreviewBlock/components";
|
import { FollowButton } from "~/components/core/CollectionPreviewBlock/components";
|
||||||
import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks";
|
import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks";
|
||||||
import { Link } from "~/components/core/Link";
|
import { Link } from "~/components/core/Link";
|
||||||
import { motion } from "framer-motion";
|
import { motion, useAnimation } from "framer-motion";
|
||||||
|
import { Preview } from "~/components/core/CollectionPreviewBlock/components";
|
||||||
import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders";
|
import { AspectRatio } from "~/components/system";
|
||||||
|
import { P3, H5, P2 } from "~/components/system/components/Typography";
|
||||||
|
import { useMounted } from "~/common/hooks";
|
||||||
|
|
||||||
const STYLES_CONTAINER = (theme) => css`
|
const STYLES_CONTAINER = (theme) => css`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -27,35 +22,25 @@ const STYLES_CONTAINER = (theme) => css`
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
height: 304px;
|
|
||||||
@media (max-width: ${theme.sizes.mobile}px) {
|
|
||||||
height: 281px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const STYLES_PREVIEW = css`
|
const STYLES_DESCRIPTION = (theme) => css`
|
||||||
flex-grow: 1;
|
position: relative;
|
||||||
background-size: cover;
|
|
||||||
overflow: hidden;
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_DESCRIPTION_CONTAINER = (theme) => css`
|
|
||||||
background-color: ${theme.semantic.bgLight};
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 9px 16px 12px;
|
|
||||||
border-radius: 0px 0px 16px 16px;
|
border-radius: 0px 0px 16px 16px;
|
||||||
box-shadow: 0 -0.5px 0.5px ${theme.system.grayLight4};
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: auto;
|
background-color: ${theme.semantic.bgLight};
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_INNER_DESCRIPTION = (theme) => css`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: ${theme.semantic.bgLight};
|
||||||
|
padding: 9px 16px 0px;
|
||||||
|
box-shadow: 0 -0.5px 0.5px ${theme.system.grayLight4};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const STYLES_SPACE_BETWEEN = css`
|
const STYLES_SPACE_BETWEEN = css`
|
||||||
@ -71,26 +56,14 @@ const STYLES_PROFILE_IMAGE = (theme) => css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const STYLES_METRICS = css`
|
const STYLES_METRICS = css`
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
padding: 4px 16px 8px;
|
||||||
${Styles.CONTAINER_CENTERED};
|
${Styles.CONTAINER_CENTERED};
|
||||||
${STYLES_SPACE_BETWEEN}
|
${STYLES_SPACE_BETWEEN}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const STYLES_PLACEHOLDER_CONTAINER = css`
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_EMPTY_CONTAINER = css`
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_CONTROLS = css`
|
const STYLES_CONTROLS = css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -104,97 +77,24 @@ const STYLES_TEXT_GRAY = (theme) => css`
|
|||||||
color: ${theme.semantic.textGray};
|
color: ${theme.semantic.textGray};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const getFileBlurHash = (file) => {
|
|
||||||
const coverImage = file?.data?.coverImage;
|
|
||||||
const coverImageBlurHash = coverImage?.data?.blurhash;
|
|
||||||
if (coverImage && isBlurhashValid(coverImageBlurHash)) return coverImageBlurHash;
|
|
||||||
|
|
||||||
const blurhash = file?.data?.blurhash;
|
|
||||||
if (isBlurhashValid(blurhash)) return blurhash;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getObjectToPreview = (objects = []) => {
|
|
||||||
let objectIdx = 0;
|
|
||||||
let isImage = false;
|
|
||||||
|
|
||||||
objects.some((object, i) => {
|
|
||||||
const isPreviewableImage = Validations.isPreviewableImage(object.data.type);
|
|
||||||
if (isPreviewableImage) (objectIdx = i), (isImage = true);
|
|
||||||
return isPreviewableImage;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { ...objects[objectIdx], isImage };
|
|
||||||
};
|
|
||||||
|
|
||||||
const Preview = ({ collection, children, ...props }) => {
|
|
||||||
const [isLoading, setLoading] = React.useState(true);
|
|
||||||
const handleOnLoaded = () => setLoading(false);
|
|
||||||
|
|
||||||
const previewerRef = React.useRef();
|
|
||||||
const { isInView } = useInView({
|
|
||||||
ref: previewerRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
const object = React.useMemo(() => getObjectToPreview(collection.objects), [collection.objects]);
|
|
||||||
|
|
||||||
const isCollectionEmpty = collection.objects.length === 0;
|
|
||||||
if (isCollectionEmpty) {
|
|
||||||
return (
|
|
||||||
<div css={STYLES_EMPTY_CONTAINER} {...props}>
|
|
||||||
{children}
|
|
||||||
<Logo style={{ height: 24, marginBottom: 8, color: Constants.system.grayLight2 }} />
|
|
||||||
<Typography.P2 color="grayLight2">This collection is empty</Typography.P2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object.isImage) {
|
|
||||||
const { coverImage } = object.data;
|
|
||||||
const blurhash = getFileBlurHash(object);
|
|
||||||
const previewImage = coverImage
|
|
||||||
? Strings.getURLfromCID(coverImage?.cid)
|
|
||||||
: Strings.getURLfromCID(object.cid);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={previewerRef} css={STYLES_PREVIEW} {...props}>
|
|
||||||
{children}
|
|
||||||
{isInView && (
|
|
||||||
<>
|
|
||||||
{isLoading && blurhash && (
|
|
||||||
<Blurhash
|
|
||||||
hash={blurhash}
|
|
||||||
style={{ position: "absolute", top: 0, left: 0 }}
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
resolutionX={32}
|
|
||||||
resolutionY={32}
|
|
||||||
punch={1}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<img src={previewImage} alt="Collection preview" onLoad={handleOnLoaded} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div css={STYLES_PREVIEW} {...props}>
|
|
||||||
{children}
|
|
||||||
<ObjectPlaceholder ratio={1} containerCss={STYLES_PLACEHOLDER_CONTAINER} file={object} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CollectionPreview({ collection, viewer, owner, onAction }) {
|
export default function CollectionPreview({ collection, viewer, owner, onAction }) {
|
||||||
const [areControlsVisible, setShowControls] = React.useState(false);
|
const [areControlsVisible, setShowControls] = React.useState(false);
|
||||||
const showControls = () => setShowControls(true);
|
const showControls = () => setShowControls(true);
|
||||||
const hideControls = () => setShowControls(false);
|
const hideControls = () => setShowControls(false);
|
||||||
|
|
||||||
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription();
|
|
||||||
const description = collection?.data?.body;
|
const description = collection?.data?.body;
|
||||||
|
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription({
|
||||||
|
disabled: !description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extendedDescriptionRef = React.useRef();
|
||||||
|
const descriptionRef = React.useRef();
|
||||||
|
|
||||||
|
const animationController = useAnimateDescription({
|
||||||
|
extendedDescriptionRef: extendedDescriptionRef,
|
||||||
|
descriptionRef: descriptionRef,
|
||||||
|
isDescriptionVisible,
|
||||||
|
});
|
||||||
|
|
||||||
const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer });
|
const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer });
|
||||||
|
|
||||||
@ -202,113 +102,191 @@ export default function CollectionPreview({ collection, viewer, owner, onAction
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div css={STYLES_CONTAINER}>
|
<div css={STYLES_CONTAINER}>
|
||||||
<Preview collection={collection} onMouseEnter={showControls} onMouseLeave={hideControls}>
|
<AspectRatio ratio={248 / 382}>
|
||||||
<motion.div
|
<div css={Styles.VERTICAL_CONTAINER}>
|
||||||
initial={{ opacity: 0 }}
|
<Preview collection={collection} onMouseEnter={showControls} onMouseLeave={hideControls}>
|
||||||
animate={{ opacity: areControlsVisible ? 1 : 0 }}
|
<motion.div
|
||||||
css={STYLES_CONTROLS}
|
|
||||||
>
|
|
||||||
<FollowButton
|
|
||||||
onClick={follow}
|
|
||||||
isFollowed={isFollowed}
|
|
||||||
followCount={followCount}
|
|
||||||
disabled={collection.ownerId === viewer?.id}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Preview>
|
|
||||||
|
|
||||||
<div style={{ position: "relative", height: 61 }}>
|
|
||||||
<motion.div
|
|
||||||
css={STYLES_DESCRIPTION_CONTAINER}
|
|
||||||
onMouseEnter={showDescription}
|
|
||||||
onMouseLeave={hideDescription}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<div css={[Styles.HORIZONTAL_CONTAINER_CENTERED, STYLES_SPACE_BETWEEN]}>
|
|
||||||
<Typography.H5 color="textBlack" nbrOflines={1} title={collection.slatename}>
|
|
||||||
{collection.slatename}
|
|
||||||
</Typography.H5>
|
|
||||||
</div>
|
|
||||||
<motion.div
|
|
||||||
style={{ marginTop: 4 }}
|
|
||||||
initial={{ height: 0 }}
|
|
||||||
animate={{
|
|
||||||
height: isDescriptionVisible ? 108 : 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 170,
|
|
||||||
damping: 26,
|
|
||||||
delay: isDescriptionVisible ? 0 : 0.25,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.P2
|
|
||||||
as={motion.p}
|
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{
|
animate={{ opacity: areControlsVisible ? 1 : 0 }}
|
||||||
opacity: isDescriptionVisible ? 1 : 0,
|
css={STYLES_CONTROLS}
|
||||||
}}
|
|
||||||
transition={{ delay: isDescriptionVisible ? 0.25 : 0 }}
|
|
||||||
color="textGrayDark"
|
|
||||||
nbrOflines={5}
|
|
||||||
>
|
>
|
||||||
{description || ""}
|
<FollowButton
|
||||||
</Typography.P2>
|
onClick={follow}
|
||||||
</motion.div>
|
isFollowed={isFollowed}
|
||||||
<div css={STYLES_METRICS}>
|
followCount={followCount}
|
||||||
<div css={[Styles.CONTAINER_CENTERED, STYLES_TEXT_GRAY]}>
|
disabled={collection.ownerId === viewer?.id}
|
||||||
<SVG.Box />
|
/>
|
||||||
<Typography.P3 style={{ marginLeft: 4 }} color="textGray">
|
</motion.div>
|
||||||
{fileCount}
|
</Preview>
|
||||||
</Typography.P3>
|
|
||||||
</div>
|
<motion.article
|
||||||
{owner && (
|
css={STYLES_DESCRIPTION}
|
||||||
<div style={{ alignItems: "end" }} css={Styles.CONTAINER_CENTERED}>
|
onMouseMove={showDescription}
|
||||||
<Link
|
onMouseLeave={hideDescription}
|
||||||
href={`/$/user/${owner.id}`}
|
>
|
||||||
onAction={onAction}
|
<div style={{ position: "relative", paddingTop: 9 }}>
|
||||||
aria-label={`Visit ${owner.username}'s profile`}
|
<H5 nbrOflines={1} style={{ visibility: "hidden" }}>
|
||||||
title={`Visit ${owner.username}'s profile`}
|
{collection.slatename}
|
||||||
|
</H5>
|
||||||
|
|
||||||
|
<div ref={descriptionRef}>
|
||||||
|
<P3
|
||||||
|
style={{ paddingTop: 3, visibility: "hidden" }}
|
||||||
|
nbrOflines={1}
|
||||||
|
color="textGrayDark"
|
||||||
>
|
>
|
||||||
<img
|
{description}
|
||||||
css={STYLES_PROFILE_IMAGE}
|
</P3>
|
||||||
src={owner?.data?.photo}
|
|
||||||
alt={`${owner.username} profile`}
|
|
||||||
onError={(e) => (e.target.src = Constants.profileDefaultPicture)}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/$/user/${owner.id}`}
|
|
||||||
onAction={onAction}
|
|
||||||
aria-label={`Visit ${owner.username}'s profile`}
|
|
||||||
title={`Visit ${owner.username}'s profile`}
|
|
||||||
>
|
|
||||||
<Typography.P3 style={{ marginLeft: 8 }} color="textGray">
|
|
||||||
{owner.username}
|
|
||||||
</Typography.P3>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
<motion.div
|
||||||
</motion.div>
|
css={STYLES_INNER_DESCRIPTION}
|
||||||
</div>
|
initial={false}
|
||||||
|
animate={isDescriptionVisible ? "hovered" : "initial"}
|
||||||
|
variants={animationController.containerVariants}
|
||||||
|
>
|
||||||
|
<H5 color="textBlack" nbrOflines={1} title={collection.slatename}>
|
||||||
|
{collection.slatename}
|
||||||
|
</H5>
|
||||||
|
{!isDescriptionVisible && (
|
||||||
|
<P3 style={{ paddingTop: 3 }} nbrOflines={1} color="textGrayDark">
|
||||||
|
{description}
|
||||||
|
</P3>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<div ref={extendedDescriptionRef}>
|
||||||
|
<P2
|
||||||
|
as={motion.p}
|
||||||
|
style={{ paddingTop: 3 }}
|
||||||
|
nbrOflines={7}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
color="textGrayDark"
|
||||||
|
animate={animationController.descriptionControls}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</P2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<Metrics owner={owner} fileCount={fileCount} onAction={onAction} />
|
||||||
|
</motion.article>
|
||||||
|
</div>
|
||||||
|
</AspectRatio>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useShowDescription = () => {
|
function Metrics({ fileCount, owner, onAction }) {
|
||||||
|
return (
|
||||||
|
<div css={STYLES_METRICS}>
|
||||||
|
<div css={[Styles.CONTAINER_CENTERED, STYLES_TEXT_GRAY]}>
|
||||||
|
<SVG.Box />
|
||||||
|
<P3 style={{ marginLeft: 4 }} color="textGray">
|
||||||
|
{fileCount}
|
||||||
|
</P3>
|
||||||
|
</div>
|
||||||
|
{owner && (
|
||||||
|
<div style={{ alignItems: "end" }} css={Styles.CONTAINER_CENTERED}>
|
||||||
|
<Link
|
||||||
|
href={`/$/user/${owner.id}`}
|
||||||
|
onAction={onAction}
|
||||||
|
aria-label={`Visit ${owner.username}'s profile`}
|
||||||
|
title={`Visit ${owner.username}'s profile`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
css={STYLES_PROFILE_IMAGE}
|
||||||
|
src={owner?.data?.photo}
|
||||||
|
alt={`${owner.username} profile`}
|
||||||
|
onError={(e) => (e.target.src = Constants.profileDefaultPicture)}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/$/user/${owner.id}`}
|
||||||
|
onAction={onAction}
|
||||||
|
aria-label={`Visit ${owner.username}'s profile`}
|
||||||
|
title={`Visit ${owner.username}'s profile`}
|
||||||
|
>
|
||||||
|
<P3 style={{ marginLeft: 8 }} color="textGray">
|
||||||
|
{owner.username}
|
||||||
|
</P3>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useShowDescription = ({ disabled }) => {
|
||||||
const [isDescriptionVisible, setShowDescription] = React.useState(false);
|
const [isDescriptionVisible, setShowDescription] = React.useState(false);
|
||||||
const timeoutId = React.useRef();
|
const timeoutId = React.useRef();
|
||||||
|
|
||||||
const showDescription = () => {
|
const showDescription = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
clearTimeout(timeoutId.current);
|
clearTimeout(timeoutId.current);
|
||||||
const id = setTimeout(() => setShowDescription(true), 250);
|
const id = setTimeout(() => setShowDescription(true), 250);
|
||||||
timeoutId.current = id;
|
timeoutId.current = id;
|
||||||
};
|
};
|
||||||
const hideDescription = () => {
|
const hideDescription = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
clearTimeout(timeoutId.current);
|
clearTimeout(timeoutId.current);
|
||||||
setShowDescription(false);
|
setShowDescription(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { isDescriptionVisible, showDescription, hideDescription };
|
return { isDescriptionVisible, showDescription, hideDescription };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useAnimateDescription = ({
|
||||||
|
extendedDescriptionRef,
|
||||||
|
descriptionRef,
|
||||||
|
isDescriptionVisible,
|
||||||
|
}) => {
|
||||||
|
const descriptionHeights = React.useRef({
|
||||||
|
extended: 0,
|
||||||
|
static: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const extendedDescriptionElement = extendedDescriptionRef.current;
|
||||||
|
const descriptionElement = descriptionRef.current;
|
||||||
|
if (descriptionElement && extendedDescriptionElement) {
|
||||||
|
descriptionHeights.current.static = descriptionElement.offsetHeight;
|
||||||
|
descriptionHeights.current.extended = extendedDescriptionElement.offsetHeight;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
initial: {
|
||||||
|
borderRadius: "0px",
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 170,
|
||||||
|
damping: 26,
|
||||||
|
delay: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hovered: {
|
||||||
|
borderRadius: "16px",
|
||||||
|
y: -descriptionHeights.current.extended + descriptionHeights.current.static,
|
||||||
|
transition: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 170,
|
||||||
|
damping: 26,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const descriptionControls = useAnimation();
|
||||||
|
|
||||||
|
useMounted(() => {
|
||||||
|
if (isDescriptionVisible) {
|
||||||
|
descriptionControls.start({ opacity: 1, transition: { delay: 0.2 } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
descriptionControls.set({ opacity: 0 });
|
||||||
|
}, [isDescriptionVisible]);
|
||||||
|
|
||||||
|
return { containerVariants, descriptionControls };
|
||||||
|
};
|
||||||
|
@ -4,6 +4,7 @@ import * as Content from "~/components/core/FontFrame/Views/content";
|
|||||||
import * as Strings from "~/common/strings";
|
import * as Strings from "~/common/strings";
|
||||||
|
|
||||||
import { generateNumberByStep } from "~/common/utilities";
|
import { generateNumberByStep } from "~/common/utilities";
|
||||||
|
import { useIsomorphicLayoutEffect } from "~/common/hooks";
|
||||||
|
|
||||||
export const useFont = ({ cid }, deps) => {
|
export const useFont = ({ cid }, deps) => {
|
||||||
const url = Strings.getURLfromCID(cid);
|
const url = Strings.getURLfromCID(cid);
|
||||||
@ -191,7 +192,7 @@ export const useFontControls = () => {
|
|||||||
|
|
||||||
const FONT_PREVIEW_STORAGE_TOKEN = "SLATE_FONT_PREVIEW_SETTINGS";
|
const FONT_PREVIEW_STORAGE_TOKEN = "SLATE_FONT_PREVIEW_SETTINGS";
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
const initialState = JSON.parse(localStorage.getItem(FONT_PREVIEW_STORAGE_TOKEN));
|
const initialState = JSON.parse(localStorage.getItem(FONT_PREVIEW_STORAGE_TOKEN));
|
||||||
if (initialState) handlers.initialState(initialState);
|
if (initialState) handlers.initialState(initialState);
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -1,59 +1,74 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Styles from "~/common/styles";
|
import * as Styles from "~/common/styles";
|
||||||
import * as Constants from "~/common/constants";
|
import * as SVG from "~/common/svg";
|
||||||
import * as Typography from "~/components/system/components/Typography";
|
|
||||||
|
|
||||||
import { H5, P3 } from "~/components/system/components/Typography";
|
import { P3 } from "~/components/system/components/Typography";
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
import { Logo } from "~/common/logo";
|
|
||||||
|
|
||||||
import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive";
|
import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive";
|
||||||
|
import LinkPlaceholder from "~/components/core/ObjectPreview/placeholders/Link";
|
||||||
|
|
||||||
const STYLES_SOURCE_LOGO = css`
|
const STYLES_SOURCE_LOGO = css`
|
||||||
height: 14px;
|
height: 12px;
|
||||||
width: 14px;
|
width: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const STYLES_EMPTY_CONTAINER = css`
|
const STYLES_PLACEHOLDER_CONTAINER = css`
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
${Styles.CONTAINER_CENTERED}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function LinkObjectPreview({ file }) {
|
const STYLES_TAG_CONTAINER = (theme) => css`
|
||||||
|
color: ${theme.semantic.textGrayLight};
|
||||||
|
svg {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
:hover svg {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
${Styles.HORIZONTAL_CONTAINER_CENTERED}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function LinkObjectPreview({ file, ratio, ...props }) {
|
||||||
const {
|
const {
|
||||||
data: { link },
|
data: { link },
|
||||||
} = file;
|
} = file;
|
||||||
|
|
||||||
const tag = (
|
const tag = (
|
||||||
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED} style={{ transform: "translateY(3px)" }}>
|
<a
|
||||||
{link.logo && (
|
css={Styles.LINK}
|
||||||
<img
|
href={file.url}
|
||||||
src={link.logo}
|
target="_blank"
|
||||||
alt="Link source logo"
|
rel="noreferrer"
|
||||||
style={{ marginRight: 4 }}
|
style={{ position: "relative", zIndex: 2 }}
|
||||||
css={STYLES_SOURCE_LOGO}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
>
|
||||||
)}
|
<div css={STYLES_TAG_CONTAINER}>
|
||||||
<P3 as="small" color="textGray">
|
{link.logo && (
|
||||||
{link.source}
|
<img
|
||||||
</P3>
|
src={link.logo}
|
||||||
</div>
|
alt="Link source logo"
|
||||||
|
style={{ marginRight: 4 }}
|
||||||
|
css={STYLES_SOURCE_LOGO}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<P3 as="small" color="textGray" nbrOflines={1}>
|
||||||
|
{link.source}
|
||||||
|
</P3>
|
||||||
|
<SVG.ExternalLink height={12} width={12} style={{ marginLeft: 4 }} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ObjectPreviewPrimitive file={file} tag={tag}>
|
<ObjectPreviewPrimitive file={file} tag={tag} {...props}>
|
||||||
{link.image ? (
|
{link.image ? (
|
||||||
<img src={link.image} alt="Link preview" css={Styles.IMAGE_FILL} />
|
<img src={link.image} alt="Link preview" css={Styles.IMAGE_FILL} />
|
||||||
) : (
|
) : (
|
||||||
<div css={STYLES_EMPTY_CONTAINER}>
|
<div css={STYLES_PLACEHOLDER_CONTAINER}>
|
||||||
<Logo style={{ height: 24, marginBottom: 8, color: Constants.system.grayLight2 }} />
|
<LinkPlaceholder ratio={ratio} />
|
||||||
<Typography.P2 color="grayLight2">No image found</Typography.P2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ObjectPreviewPrimitive>
|
</ObjectPreviewPrimitive>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import * as Styles from "~/common/styles";
|
||||||
|
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
import { H5, P3 } from "~/components/system/components/Typography";
|
import { H5, P2, P3 } from "~/components/system/components/Typography";
|
||||||
import { AspectRatio } from "~/components/system";
|
import { AspectRatio } from "~/components/system";
|
||||||
// import { LikeButton, SaveButton } from "./components";
|
// import { LikeButton, SaveButton } from "./components";
|
||||||
// import { useLikeHandler, useSaveHandler } from "~/common/hooks";
|
// import { useLikeHandler, useSaveHandler } from "~/common/hooks";
|
||||||
import { motion } from "framer-motion";
|
import { motion, useAnimation } from "framer-motion";
|
||||||
|
import { useMounted } from "~/common/hooks";
|
||||||
|
|
||||||
import ImageObjectPreview from "./ImageObjectPreview";
|
import ImageObjectPreview from "./ImageObjectPreview";
|
||||||
|
|
||||||
@ -13,7 +15,7 @@ const STYLES_WRAPPER = (theme) => css`
|
|||||||
position: relative;
|
position: relative;
|
||||||
background-color: ${theme.semantic.bgLight};
|
background-color: ${theme.semantic.bgLight};
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
box-shadow: 0 0 0 0.5px ${theme.system.grayLight4}, ${theme.shadow.lightSmall};
|
box-shadow: 0 0 0 0.5px ${theme.system.grayLight4}, ${theme.shadow.card};
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -21,14 +23,10 @@ const STYLES_WRAPPER = (theme) => css`
|
|||||||
|
|
||||||
const STYLES_DESCRIPTION = (theme) => css`
|
const STYLES_DESCRIPTION = (theme) => css`
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 -0.5px 0.5px ${theme.system.grayLight4};
|
|
||||||
border-radius: 0px 0px 16px 16px;
|
border-radius: 0px 0px 16px 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: ${theme.semantic.bgLight};
|
background-color: ${theme.semantic.bgLight};
|
||||||
border-radius: 16px;
|
|
||||||
height: calc(170px + 61px);
|
|
||||||
padding: 9px 16px 8px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
@media (max-width: ${theme.sizes.mobile}px) {
|
@media (max-width: ${theme.sizes.mobile}px) {
|
||||||
@ -36,10 +34,28 @@ const STYLES_DESCRIPTION = (theme) => css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const STYLES_INNER_DESCRIPTION = (theme) => css`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: ${theme.semantic.bgLight};
|
||||||
|
padding: 9px 16px 0px;
|
||||||
|
box-shadow: 0 -0.5px 0.5px ${theme.system.grayLight4};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_TAG = css`
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 16px 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
const STYLES_PREVIEW = css`
|
const STYLES_PREVIEW = css`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 0.5px;
|
bottom: 0.5px;
|
||||||
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const STYLES_SELECTED_RING = (theme) => css`
|
const STYLES_SELECTED_RING = (theme) => css`
|
||||||
@ -71,7 +87,6 @@ export default function ObjectPreviewPrimitive({
|
|||||||
isImage,
|
isImage,
|
||||||
onAction,
|
onAction,
|
||||||
}) {
|
}) {
|
||||||
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription();
|
|
||||||
// const { like, isLiked, likeCount } = useLikeHandler({ file, viewer });
|
// const { like, isLiked, likeCount } = useLikeHandler({ file, viewer });
|
||||||
// const { save, isSaved, saveCount } = useSaveHandler({ file, viewer });
|
// const { save, isSaved, saveCount } = useSaveHandler({ file, viewer });
|
||||||
// const showSaveButton = viewer?.id !== file?.ownerId;
|
// const showSaveButton = viewer?.id !== file?.ownerId;
|
||||||
@ -80,7 +95,20 @@ export default function ObjectPreviewPrimitive({
|
|||||||
// const showControls = () => setShowControls(true);
|
// const showControls = () => setShowControls(true);
|
||||||
// const hideControls = () => setShowControls(false);
|
// const hideControls = () => setShowControls(false);
|
||||||
|
|
||||||
const body = file?.data?.body;
|
const description = file?.data?.body;
|
||||||
|
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription({
|
||||||
|
disabled: !description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extendedDescriptionRef = React.useRef();
|
||||||
|
const descriptionRef = React.useRef();
|
||||||
|
|
||||||
|
const animationController = useAnimateDescription({
|
||||||
|
extendedDescriptionRef: extendedDescriptionRef,
|
||||||
|
descriptionRef: descriptionRef,
|
||||||
|
isDescriptionVisible,
|
||||||
|
});
|
||||||
|
|
||||||
const { isLink } = file;
|
const { isLink } = file;
|
||||||
|
|
||||||
const title = file.data.name || file.filename;
|
const title = file.data.name || file.filename;
|
||||||
@ -99,11 +127,13 @@ export default function ObjectPreviewPrimitive({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div css={[STYLES_WRAPPER, isSelected && STYLES_SELECTED_RING]}>
|
<div css={[STYLES_WRAPPER, isSelected && STYLES_SELECTED_RING]}>
|
||||||
<div
|
<AspectRatio ratio={248 / 248}>
|
||||||
css={STYLES_PREVIEW}
|
<div css={Styles.VERTICAL_CONTAINER}>
|
||||||
// onMouseEnter={showControls} onMouseLeave={hideControls}
|
<div
|
||||||
>
|
css={STYLES_PREVIEW}
|
||||||
{/* <AnimatePresence>
|
// onMouseEnter={showControls} onMouseLeave={hideControls}
|
||||||
|
>
|
||||||
|
{/* <AnimatePresence>
|
||||||
{areControlsVisible && (
|
{areControlsVisible && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@ -118,63 +148,150 @@ export default function ObjectPreviewPrimitive({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence> */}
|
</AnimatePresence> */}
|
||||||
<AspectRatio ratio={248 / 248}>
|
{children}
|
||||||
<div>{children}</div>
|
|
||||||
</AspectRatio>
|
|
||||||
</div>
|
|
||||||
<div style={{ maxHeight: 61 }}>
|
|
||||||
<motion.article
|
|
||||||
css={STYLES_DESCRIPTION}
|
|
||||||
onMouseMove={showDescription}
|
|
||||||
onMouseLeave={hideDescription}
|
|
||||||
initial={{ y: 0 }}
|
|
||||||
animate={{
|
|
||||||
y: isDescriptionVisible ? -170 : 0,
|
|
||||||
borderRadius: isDescriptionVisible ? "16px" : "0px",
|
|
||||||
}}
|
|
||||||
transition={{ type: "spring", stiffness: 170, damping: 26 }}
|
|
||||||
>
|
|
||||||
<H5 as="h2" nbrOflines={1} color="textBlack" title={title}>
|
|
||||||
{title}
|
|
||||||
</H5>
|
|
||||||
<div style={{ marginTop: 3, display: "flex" }}>
|
|
||||||
{typeof tag === "string" ? (
|
|
||||||
<P3 as="small" css={STYLES_UPPERCASE} color="textGray">
|
|
||||||
{tag}
|
|
||||||
</P3>
|
|
||||||
) : (
|
|
||||||
tag
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<H5
|
|
||||||
as={motion.p}
|
<motion.article
|
||||||
initial={{ opacity: 0 }}
|
css={STYLES_DESCRIPTION}
|
||||||
animate={{ opacity: isDescriptionVisible ? 1 : 0 }}
|
onMouseMove={showDescription}
|
||||||
style={{ marginTop: 5 }}
|
onMouseLeave={hideDescription}
|
||||||
nbrOflines={8}
|
|
||||||
color="textGrayDark"
|
|
||||||
>
|
>
|
||||||
{body || ""}
|
<div style={{ position: "relative", paddingTop: 9 }}>
|
||||||
</H5>
|
<H5 as="h2" nbrOflines={1} style={{ visibility: "hidden" }}>
|
||||||
</motion.article>
|
{title}
|
||||||
</div>
|
</H5>
|
||||||
|
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
<H5 as="h2" nbrOflines={1} color="textBlack" title={title}>
|
||||||
|
{title}
|
||||||
|
</H5>
|
||||||
|
{!isDescriptionVisible && (
|
||||||
|
<P3 style={{ paddingTop: 3 }} nbrOflines={1} color="textGrayDark">
|
||||||
|
{description}
|
||||||
|
</P3>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<div ref={extendedDescriptionRef}>
|
||||||
|
<P2
|
||||||
|
as={motion.p}
|
||||||
|
style={{ paddingTop: 3 }}
|
||||||
|
nbrOflines={7}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
color="textGrayDark"
|
||||||
|
animate={animationController.descriptionControls}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</P2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TagComponent tag={tag} />
|
||||||
|
</motion.article>
|
||||||
|
</div>
|
||||||
|
</AspectRatio>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useShowDescription = () => {
|
const TagComponent = ({ tag }) => (
|
||||||
|
<div css={STYLES_TAG}>
|
||||||
|
{typeof tag === "string" ? (
|
||||||
|
<P3 as="small" css={STYLES_UPPERCASE} color="textGray">
|
||||||
|
{tag}
|
||||||
|
</P3>
|
||||||
|
) : (
|
||||||
|
tag
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const useShowDescription = ({ disabled }) => {
|
||||||
const [isDescriptionVisible, setShowDescription] = React.useState(false);
|
const [isDescriptionVisible, setShowDescription] = React.useState(false);
|
||||||
const timeoutId = React.useRef();
|
const timeoutId = React.useRef();
|
||||||
|
|
||||||
const showDescription = () => {
|
const showDescription = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
clearTimeout(timeoutId.current);
|
clearTimeout(timeoutId.current);
|
||||||
const id = setTimeout(() => setShowDescription(true), 250);
|
const id = setTimeout(() => setShowDescription(true), 250);
|
||||||
timeoutId.current = id;
|
timeoutId.current = id;
|
||||||
};
|
};
|
||||||
const hideDescription = () => {
|
const hideDescription = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
clearTimeout(timeoutId.current);
|
clearTimeout(timeoutId.current);
|
||||||
setShowDescription(false);
|
setShowDescription(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { isDescriptionVisible, showDescription, hideDescription };
|
return { isDescriptionVisible, showDescription, hideDescription };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useAnimateDescription = ({
|
||||||
|
extendedDescriptionRef,
|
||||||
|
descriptionRef,
|
||||||
|
isDescriptionVisible,
|
||||||
|
}) => {
|
||||||
|
const descriptionHeights = React.useRef({
|
||||||
|
extended: 0,
|
||||||
|
static: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const extendedDescriptionElement = extendedDescriptionRef.current;
|
||||||
|
const descriptionElement = descriptionRef.current;
|
||||||
|
if (descriptionElement && extendedDescriptionElement) {
|
||||||
|
descriptionHeights.current.static = descriptionElement.offsetHeight;
|
||||||
|
descriptionHeights.current.extended = extendedDescriptionElement.offsetHeight;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
initial: {
|
||||||
|
borderRadius: "0px",
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 170,
|
||||||
|
damping: 26,
|
||||||
|
delay: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hovered: {
|
||||||
|
borderRadius: "16px",
|
||||||
|
y: -descriptionHeights.current.extended + descriptionHeights.current.static,
|
||||||
|
transition: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 170,
|
||||||
|
damping: 26,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const descriptionControls = useAnimation();
|
||||||
|
|
||||||
|
useMounted(() => {
|
||||||
|
if (isDescriptionVisible) {
|
||||||
|
descriptionControls.start({ opacity: 1, transition: { delay: 0.2 } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
descriptionControls.set({ opacity: 0 });
|
||||||
|
}, [isDescriptionVisible]);
|
||||||
|
|
||||||
|
return { containerVariants, descriptionControls };
|
||||||
|
};
|
||||||
|
@ -5,6 +5,7 @@ import * as Styles from "~/common/styles";
|
|||||||
import * as Utilities from "~/common/utilities";
|
import * as Utilities from "~/common/utilities";
|
||||||
|
|
||||||
import { P3 } from "~/components/system";
|
import { P3 } from "~/components/system";
|
||||||
|
import { useIsomorphicLayoutEffect } from "~/common/hooks";
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
|
|
||||||
import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File";
|
import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File";
|
||||||
@ -30,7 +31,7 @@ const STYLES_TEXT_PREVIEW = (theme) =>
|
|||||||
export default function TextObjectPreview({ url, file, ...props }) {
|
export default function TextObjectPreview({ url, file, ...props }) {
|
||||||
const [{ content, error }, setState] = React.useState({ content: "", error: undefined });
|
const [{ content, error }, setState] = React.useState({ content: "", error: undefined });
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const content = await res.text();
|
const content = await res.text();
|
||||||
|
@ -28,8 +28,10 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
|
|
||||||
const url = Strings.getURLfromCID(file.cid);
|
const url = Strings.getURLfromCID(file.cid);
|
||||||
|
|
||||||
|
const PLACEHOLDER_RATIO = 1.3;
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
return <LinkObjectPreview file={file} />;
|
return <LinkObjectPreview file={file} ratio={PLACEHOLDER_RATIO} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Validations.isPreviewableImage(type)) {
|
if (Validations.isPreviewableImage(type)) {
|
||||||
@ -40,7 +42,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
const tag = type.split("/")[1];
|
const tag = type.split("/")[1];
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
||||||
<VideoPlaceholder />
|
<VideoPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -48,7 +50,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
if (Validations.isPdfType(type)) {
|
if (Validations.isPdfType(type)) {
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag="PDF" file={file} {...props}>
|
<PlaceholderWrapper tag="PDF" file={file} {...props}>
|
||||||
<PdfPlaceholder />
|
<PdfPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -57,7 +59,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
const tag = Utilities.getFileExtension(file.filename) || "audio";
|
const tag = Utilities.getFileExtension(file.filename) || "audio";
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
||||||
<AudioPlaceholder />
|
<AudioPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,7 +67,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
if (type === "application/epub+zip") {
|
if (type === "application/epub+zip") {
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag="epub" file={file} {...props}>
|
<PlaceholderWrapper tag="epub" file={file} {...props}>
|
||||||
<EbookPlaceholder />
|
<EbookPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -73,7 +75,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
if (file.filename.endsWith(".key")) {
|
if (file.filename.endsWith(".key")) {
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag="keynote" file={file} {...props}>
|
<PlaceholderWrapper tag="keynote" file={file} {...props}>
|
||||||
<KeynotePlaceholder />
|
<KeynotePlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -82,7 +84,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
const tag = Utilities.getFileExtension(file.filename) || "code";
|
const tag = Utilities.getFileExtension(file.filename) || "code";
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
||||||
<CodePlaceholder />
|
<CodePlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -98,14 +100,14 @@ const ObjectPreview = ({ file, ...props }) => {
|
|||||||
if (Validations.is3dFile(file.filename)) {
|
if (Validations.is3dFile(file.filename)) {
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag="3d" file={file} {...props}>
|
<PlaceholderWrapper tag="3d" file={file} {...props}>
|
||||||
<Object3DPlaceholder />
|
<Object3DPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlaceholderWrapper tag="file" file={file} {...props}>
|
<PlaceholderWrapper tag="file" file={file} {...props}>
|
||||||
<FilePlaceholder />
|
<FilePlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||||
</PlaceholderWrapper>
|
</PlaceholderWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
72
components/core/ObjectPreview/placeholders/Link.js
Normal file
72
components/core/ObjectPreview/placeholders/Link.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { css } from "@emotion/react";
|
||||||
|
|
||||||
|
export default function LinkPlaceholder({ ratio = 1, ...props }) {
|
||||||
|
const STYLES_PLACEHOLDER = React.useMemo(
|
||||||
|
() => css`
|
||||||
|
overflow: visible !important;
|
||||||
|
width: ${(96 / 248) * 100 * ratio}%;
|
||||||
|
height: ${(64 / 248) * 100 * ratio}%;
|
||||||
|
`,
|
||||||
|
[ratio]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={96}
|
||||||
|
height={64}
|
||||||
|
viewBox="62 50 96 64"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
css={STYLES_PLACEHOLDER}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g filter="url(#prefix__filter0_d_link)">
|
||||||
|
<rect x={64} y={52} width={96} height={64} rx={8} fill="#fff" />
|
||||||
|
</g>
|
||||||
|
<path d="M64 60a8 8 0 018-8h80a8 8 0 018 8H64z" fill="#E5E8EA" />
|
||||||
|
<path
|
||||||
|
d="M121 92v-8a2.001 2.001 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4a2.003 2.003 0 00-1 1.73v8a2.002 2.002 0 001 1.73l7 4a2 2 0 002 0l7-4a2.003 2.003 0 001-1.73z"
|
||||||
|
fill="#E5E8EA"
|
||||||
|
stroke="#fff"
|
||||||
|
strokeWidth={1.25}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M103.27 82.96l8.73 5.05 8.73-5.05M112 98.08V88"
|
||||||
|
stroke="#fff"
|
||||||
|
strokeWidth={1.25}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx={70.5} cy={56.5} r={1.5} fill="#FF4530" />
|
||||||
|
<circle cx={75.5} cy={56.5} r={1.5} fill="#C4C4C4" />
|
||||||
|
<circle cx={80.5} cy={56.5} r={1.5} fill="#34D159" />
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
id="prefix__filter0_d_link"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={224}
|
||||||
|
height={192}
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
colorInterpolationFilters="sRGB"
|
||||||
|
>
|
||||||
|
<feFlood floodOpacity={0} result="BackgroundImageFix" />
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceAlpha"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||||
|
result="hardAlpha"
|
||||||
|
/>
|
||||||
|
<feOffset dy={12} />
|
||||||
|
<feGaussianBlur stdDeviation={32} />
|
||||||
|
<feColorMatrix values="0 0 0 0 0.682353 0 0 0 0 0.690196 0 0 0 0 0.698039 0 0 0 0.3 0" />
|
||||||
|
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||||
|
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
@ -13,6 +13,7 @@ import KeynotePlaceholder from "./Keynote";
|
|||||||
import Object3DPlaceholder from "./3D";
|
import Object3DPlaceholder from "./3D";
|
||||||
import FilePlaceholder from "./File";
|
import FilePlaceholder from "./File";
|
||||||
import VideoPlaceholder from "./Video";
|
import VideoPlaceholder from "./Video";
|
||||||
|
import LinkPlaceholder from "./Link";
|
||||||
|
|
||||||
const STYLES_PLACEHOLDER_CONTAINER = (theme) => css`
|
const STYLES_PLACEHOLDER_CONTAINER = (theme) => css`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -39,8 +40,11 @@ const STYLES_TAG = (theme) => css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const PlaceholderPrimitive = ({ file, ratio }) => {
|
const PlaceholderPrimitive = ({ file, ratio }) => {
|
||||||
const { type } = file.data;
|
const { type, link } = file.data;
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
return <LinkPlaceholder ratio={ratio} />;
|
||||||
|
}
|
||||||
if (type.startsWith("video/")) {
|
if (type.startsWith("video/")) {
|
||||||
return <VideoPlaceholder ratio={ratio} />;
|
return <VideoPlaceholder ratio={ratio} />;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ const truncateElements = (nbrOfLines) =>
|
|||||||
nbrOfLines &&
|
nbrOfLines &&
|
||||||
css`
|
css`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 1.5;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
-webkit-line-clamp: ${nbrOfLines};
|
-webkit-line-clamp: ${nbrOfLines};
|
||||||
|
Loading…
Reference in New Issue
Block a user