mirror of
https://github.com/filecoin-project/slate.git
synced 2024-12-24 17:44:50 +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)",
|
||||
darkMedium: "0px 8px 32px 0 rgba(99, 101, 102, 0.2)",
|
||||
darkLarge: "0px 12px 64px 0 rgba(99, 101, 102, 0.3)",
|
||||
card: "0px 0px 32px #E5E8EA;",
|
||||
};
|
||||
|
||||
export const zindex = {
|
||||
@ -204,12 +205,12 @@ export const grids = {
|
||||
},
|
||||
},
|
||||
object: {
|
||||
desktop: { width: 248, rowGap: 16 },
|
||||
mobile: { width: 166, rowGap: 8 },
|
||||
desktop: { width: 248, rowGap: 20 },
|
||||
mobile: { width: 166, rowGap: 12 },
|
||||
},
|
||||
collection: {
|
||||
desktop: { width: 432, rowGap: 16 },
|
||||
mobile: { width: 300, rowGap: 8 },
|
||||
desktop: { width: 382, rowGap: 16 },
|
||||
mobile: { width: 280, rowGap: 8 },
|
||||
},
|
||||
profile: {
|
||||
desktop: { width: 248, rowGap: 16 },
|
||||
|
@ -5,7 +5,7 @@ import * as Events from "~/common/custom-events";
|
||||
|
||||
export const useMounted = (callback, depedencies) => {
|
||||
const mountedRef = React.useRef(false);
|
||||
React.useLayoutEffect(() => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (mountedRef.current && callback) {
|
||||
callback();
|
||||
}
|
||||
@ -230,7 +230,7 @@ export const useIntersection = ({ onIntersect, ref }, dependencies = []) => {
|
||||
const onIntersectRef = React.useRef();
|
||||
onIntersectRef.current = onIntersect;
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const lazyObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
@ -404,3 +404,10 @@ export function useMemoCompare(next, compare) {
|
||||
|
||||
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 Preview } from "./Preview";
|
||||
|
@ -1,22 +1,17 @@
|
||||
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 Strings from "~/common/strings";
|
||||
import * as Constants from "~/common/constants";
|
||||
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 { FollowButton } from "~/components/core/CollectionPreviewBlock/components";
|
||||
import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks";
|
||||
import { Link } from "~/components/core/Link";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders";
|
||||
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";
|
||||
|
||||
const STYLES_CONTAINER = (theme) => css`
|
||||
position: relative;
|
||||
@ -27,35 +22,25 @@ const STYLES_CONTAINER = (theme) => css`
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
height: 304px;
|
||||
@media (max-width: ${theme.sizes.mobile}px) {
|
||||
height: 281px;
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_PREVIEW = css`
|
||||
flex-grow: 1;
|
||||
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;
|
||||
const STYLES_DESCRIPTION = (theme) => css`
|
||||
position: relative;
|
||||
border-radius: 0px 0px 16px 16px;
|
||||
box-shadow: 0 -0.5px 0.5px ${theme.system.grayLight4};
|
||||
box-sizing: border-box;
|
||||
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`
|
||||
@ -71,26 +56,14 @@ const STYLES_PROFILE_IMAGE = (theme) => css`
|
||||
`;
|
||||
|
||||
const STYLES_METRICS = css`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: auto;
|
||||
padding: 4px 16px 8px;
|
||||
${Styles.CONTAINER_CENTERED};
|
||||
${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`
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
@ -104,97 +77,24 @@ const STYLES_TEXT_GRAY = (theme) => css`
|
||||
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 }) {
|
||||
const [areControlsVisible, setShowControls] = React.useState(false);
|
||||
const showControls = () => setShowControls(true);
|
||||
const hideControls = () => setShowControls(false);
|
||||
|
||||
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription();
|
||||
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 });
|
||||
|
||||
@ -202,113 +102,191 @@ export default function CollectionPreview({ collection, viewer, owner, onAction
|
||||
|
||||
return (
|
||||
<div css={STYLES_CONTAINER}>
|
||||
<Preview collection={collection} onMouseEnter={showControls} onMouseLeave={hideControls}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: areControlsVisible ? 1 : 0 }}
|
||||
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}
|
||||
<AspectRatio ratio={248 / 382}>
|
||||
<div css={Styles.VERTICAL_CONTAINER}>
|
||||
<Preview collection={collection} onMouseEnter={showControls} onMouseLeave={hideControls}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: isDescriptionVisible ? 1 : 0,
|
||||
}}
|
||||
transition={{ delay: isDescriptionVisible ? 0.25 : 0 }}
|
||||
color="textGrayDark"
|
||||
nbrOflines={5}
|
||||
animate={{ opacity: areControlsVisible ? 1 : 0 }}
|
||||
css={STYLES_CONTROLS}
|
||||
>
|
||||
{description || ""}
|
||||
</Typography.P2>
|
||||
</motion.div>
|
||||
<div css={STYLES_METRICS}>
|
||||
<div css={[Styles.CONTAINER_CENTERED, STYLES_TEXT_GRAY]}>
|
||||
<SVG.Box />
|
||||
<Typography.P3 style={{ marginLeft: 4 }} color="textGray">
|
||||
{fileCount}
|
||||
</Typography.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`}
|
||||
<FollowButton
|
||||
onClick={follow}
|
||||
isFollowed={isFollowed}
|
||||
followCount={followCount}
|
||||
disabled={collection.ownerId === viewer?.id}
|
||||
/>
|
||||
</motion.div>
|
||||
</Preview>
|
||||
|
||||
<motion.article
|
||||
css={STYLES_DESCRIPTION}
|
||||
onMouseMove={showDescription}
|
||||
onMouseLeave={hideDescription}
|
||||
>
|
||||
<div style={{ position: "relative", paddingTop: 9 }}>
|
||||
<H5 nbrOflines={1} style={{ visibility: "hidden" }}>
|
||||
{collection.slatename}
|
||||
</H5>
|
||||
|
||||
<div ref={descriptionRef}>
|
||||
<P3
|
||||
style={{ paddingTop: 3, visibility: "hidden" }}
|
||||
nbrOflines={1}
|
||||
color="textGrayDark"
|
||||
>
|
||||
<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`}
|
||||
>
|
||||
<Typography.P3 style={{ marginLeft: 8 }} color="textGray">
|
||||
{owner.username}
|
||||
</Typography.P3>
|
||||
</Link>
|
||||
{description}
|
||||
</P3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
css={STYLES_INNER_DESCRIPTION}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 timeoutId = React.useRef();
|
||||
|
||||
const showDescription = () => {
|
||||
if (disabled) return;
|
||||
|
||||
clearTimeout(timeoutId.current);
|
||||
const id = setTimeout(() => setShowDescription(true), 250);
|
||||
timeoutId.current = id;
|
||||
};
|
||||
const hideDescription = () => {
|
||||
if (disabled) return;
|
||||
|
||||
clearTimeout(timeoutId.current);
|
||||
setShowDescription(false);
|
||||
};
|
||||
|
||||
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 { generateNumberByStep } from "~/common/utilities";
|
||||
import { useIsomorphicLayoutEffect } from "~/common/hooks";
|
||||
|
||||
export const useFont = ({ cid }, deps) => {
|
||||
const url = Strings.getURLfromCID(cid);
|
||||
@ -191,7 +192,7 @@ export const useFontControls = () => {
|
||||
|
||||
const FONT_PREVIEW_STORAGE_TOKEN = "SLATE_FONT_PREVIEW_SETTINGS";
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const initialState = JSON.parse(localStorage.getItem(FONT_PREVIEW_STORAGE_TOKEN));
|
||||
if (initialState) handlers.initialState(initialState);
|
||||
}, []);
|
||||
|
@ -1,59 +1,74 @@
|
||||
import * as React from "react";
|
||||
import * as Styles from "~/common/styles";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Typography from "~/components/system/components/Typography";
|
||||
import * as SVG from "~/common/svg";
|
||||
|
||||
import { H5, P3 } from "~/components/system/components/Typography";
|
||||
import { P3 } from "~/components/system/components/Typography";
|
||||
import { css } from "@emotion/react";
|
||||
import { Logo } from "~/common/logo";
|
||||
|
||||
import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive";
|
||||
import LinkPlaceholder from "~/components/core/ObjectPreview/placeholders/Link";
|
||||
|
||||
const STYLES_SOURCE_LOGO = css`
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const STYLES_EMPTY_CONTAINER = css`
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
const STYLES_PLACEHOLDER_CONTAINER = css`
|
||||
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 {
|
||||
data: { link },
|
||||
} = file;
|
||||
|
||||
const tag = (
|
||||
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED} style={{ transform: "translateY(3px)" }}>
|
||||
{link.logo && (
|
||||
<img
|
||||
src={link.logo}
|
||||
alt="Link source logo"
|
||||
style={{ marginRight: 4 }}
|
||||
css={STYLES_SOURCE_LOGO}
|
||||
/>
|
||||
)}
|
||||
<P3 as="small" color="textGray">
|
||||
{link.source}
|
||||
</P3>
|
||||
</div>
|
||||
<a
|
||||
css={Styles.LINK}
|
||||
href={file.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ position: "relative", zIndex: 2 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div css={STYLES_TAG_CONTAINER}>
|
||||
{link.logo && (
|
||||
<img
|
||||
src={link.logo}
|
||||
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 (
|
||||
<ObjectPreviewPrimitive file={file} tag={tag}>
|
||||
<ObjectPreviewPrimitive file={file} tag={tag} {...props}>
|
||||
{link.image ? (
|
||||
<img src={link.image} alt="Link preview" css={Styles.IMAGE_FILL} />
|
||||
) : (
|
||||
<div css={STYLES_EMPTY_CONTAINER}>
|
||||
<Logo style={{ height: 24, marginBottom: 8, color: Constants.system.grayLight2 }} />
|
||||
<Typography.P2 color="grayLight2">No image found</Typography.P2>
|
||||
<div css={STYLES_PLACEHOLDER_CONTAINER}>
|
||||
<LinkPlaceholder ratio={ratio} />
|
||||
</div>
|
||||
)}
|
||||
</ObjectPreviewPrimitive>
|
||||
|
@ -1,11 +1,13 @@
|
||||
import * as React from "react";
|
||||
import * as Styles from "~/common/styles";
|
||||
|
||||
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 { LikeButton, SaveButton } from "./components";
|
||||
// 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";
|
||||
|
||||
@ -13,7 +15,7 @@ const STYLES_WRAPPER = (theme) => css`
|
||||
position: relative;
|
||||
background-color: ${theme.semantic.bgLight};
|
||||
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;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
@ -21,14 +23,10 @@ const STYLES_WRAPPER = (theme) => css`
|
||||
|
||||
const STYLES_DESCRIPTION = (theme) => css`
|
||||
position: relative;
|
||||
box-shadow: 0 -0.5px 0.5px ${theme.system.grayLight4};
|
||||
border-radius: 0px 0px 16px 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background-color: ${theme.semantic.bgLight};
|
||||
border-radius: 16px;
|
||||
height: calc(170px + 61px);
|
||||
padding: 9px 16px 8px;
|
||||
z-index: 1;
|
||||
|
||||
@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`
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
bottom: 0.5px;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const STYLES_SELECTED_RING = (theme) => css`
|
||||
@ -71,7 +87,6 @@ export default function ObjectPreviewPrimitive({
|
||||
isImage,
|
||||
onAction,
|
||||
}) {
|
||||
const { isDescriptionVisible, showDescription, hideDescription } = useShowDescription();
|
||||
// const { like, isLiked, likeCount } = useLikeHandler({ file, viewer });
|
||||
// const { save, isSaved, saveCount } = useSaveHandler({ file, viewer });
|
||||
// const showSaveButton = viewer?.id !== file?.ownerId;
|
||||
@ -80,7 +95,20 @@ export default function ObjectPreviewPrimitive({
|
||||
// const showControls = () => setShowControls(true);
|
||||
// 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 title = file.data.name || file.filename;
|
||||
@ -99,11 +127,13 @@ export default function ObjectPreviewPrimitive({
|
||||
|
||||
return (
|
||||
<div css={[STYLES_WRAPPER, isSelected && STYLES_SELECTED_RING]}>
|
||||
<div
|
||||
css={STYLES_PREVIEW}
|
||||
// onMouseEnter={showControls} onMouseLeave={hideControls}
|
||||
>
|
||||
{/* <AnimatePresence>
|
||||
<AspectRatio ratio={248 / 248}>
|
||||
<div css={Styles.VERTICAL_CONTAINER}>
|
||||
<div
|
||||
css={STYLES_PREVIEW}
|
||||
// onMouseEnter={showControls} onMouseLeave={hideControls}
|
||||
>
|
||||
{/* <AnimatePresence>
|
||||
{areControlsVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
@ -118,63 +148,150 @@ export default function ObjectPreviewPrimitive({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence> */}
|
||||
<AspectRatio ratio={248 / 248}>
|
||||
<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
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
<H5
|
||||
as={motion.p}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isDescriptionVisible ? 1 : 0 }}
|
||||
style={{ marginTop: 5 }}
|
||||
nbrOflines={8}
|
||||
color="textGrayDark"
|
||||
|
||||
<motion.article
|
||||
css={STYLES_DESCRIPTION}
|
||||
onMouseMove={showDescription}
|
||||
onMouseLeave={hideDescription}
|
||||
>
|
||||
{body || ""}
|
||||
</H5>
|
||||
</motion.article>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 timeoutId = React.useRef();
|
||||
|
||||
const showDescription = () => {
|
||||
if (disabled) return;
|
||||
|
||||
clearTimeout(timeoutId.current);
|
||||
const id = setTimeout(() => setShowDescription(true), 250);
|
||||
timeoutId.current = id;
|
||||
};
|
||||
const hideDescription = () => {
|
||||
if (disabled) return;
|
||||
|
||||
clearTimeout(timeoutId.current);
|
||||
setShowDescription(false);
|
||||
};
|
||||
|
||||
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 { P3 } from "~/components/system";
|
||||
import { useIsomorphicLayoutEffect } from "~/common/hooks";
|
||||
import { css } from "@emotion/react";
|
||||
|
||||
import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File";
|
||||
@ -30,7 +31,7 @@ const STYLES_TEXT_PREVIEW = (theme) =>
|
||||
export default function TextObjectPreview({ url, file, ...props }) {
|
||||
const [{ content, error }, setState] = React.useState({ content: "", error: undefined });
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
fetch(url)
|
||||
.then(async (res) => {
|
||||
const content = await res.text();
|
||||
|
@ -28,8 +28,10 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
|
||||
const url = Strings.getURLfromCID(file.cid);
|
||||
|
||||
const PLACEHOLDER_RATIO = 1.3;
|
||||
|
||||
if (link) {
|
||||
return <LinkObjectPreview file={file} />;
|
||||
return <LinkObjectPreview file={file} ratio={PLACEHOLDER_RATIO} {...props} />;
|
||||
}
|
||||
|
||||
if (Validations.isPreviewableImage(type)) {
|
||||
@ -40,7 +42,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
const tag = type.split("/")[1];
|
||||
return (
|
||||
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
||||
<VideoPlaceholder />
|
||||
<VideoPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</PlaceholderWrapper>
|
||||
);
|
||||
}
|
||||
@ -48,7 +50,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
if (Validations.isPdfType(type)) {
|
||||
return (
|
||||
<PlaceholderWrapper tag="PDF" file={file} {...props}>
|
||||
<PdfPlaceholder />
|
||||
<PdfPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</PlaceholderWrapper>
|
||||
);
|
||||
}
|
||||
@ -57,7 +59,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
const tag = Utilities.getFileExtension(file.filename) || "audio";
|
||||
return (
|
||||
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
||||
<AudioPlaceholder />
|
||||
<AudioPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</PlaceholderWrapper>
|
||||
);
|
||||
}
|
||||
@ -65,7 +67,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
if (type === "application/epub+zip") {
|
||||
return (
|
||||
<PlaceholderWrapper tag="epub" file={file} {...props}>
|
||||
<EbookPlaceholder />
|
||||
<EbookPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</PlaceholderWrapper>
|
||||
);
|
||||
}
|
||||
@ -73,7 +75,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
if (file.filename.endsWith(".key")) {
|
||||
return (
|
||||
<PlaceholderWrapper tag="keynote" file={file} {...props}>
|
||||
<KeynotePlaceholder />
|
||||
<KeynotePlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</PlaceholderWrapper>
|
||||
);
|
||||
}
|
||||
@ -82,7 +84,7 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
const tag = Utilities.getFileExtension(file.filename) || "code";
|
||||
return (
|
||||
<PlaceholderWrapper tag={tag} file={file} {...props}>
|
||||
<CodePlaceholder />
|
||||
<CodePlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</PlaceholderWrapper>
|
||||
);
|
||||
}
|
||||
@ -98,14 +100,14 @@ const ObjectPreview = ({ file, ...props }) => {
|
||||
if (Validations.is3dFile(file.filename)) {
|
||||
return (
|
||||
<PlaceholderWrapper tag="3d" file={file} {...props}>
|
||||
<Object3DPlaceholder />
|
||||
<Object3DPlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</PlaceholderWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PlaceholderWrapper tag="file" file={file} {...props}>
|
||||
<FilePlaceholder />
|
||||
<FilePlaceholder ratio={PLACEHOLDER_RATIO} />
|
||||
</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 FilePlaceholder from "./File";
|
||||
import VideoPlaceholder from "./Video";
|
||||
import LinkPlaceholder from "./Link";
|
||||
|
||||
const STYLES_PLACEHOLDER_CONTAINER = (theme) => css`
|
||||
position: relative;
|
||||
@ -39,8 +40,11 @@ const STYLES_TAG = (theme) => css`
|
||||
`;
|
||||
|
||||
const PlaceholderPrimitive = ({ file, ratio }) => {
|
||||
const { type } = file.data;
|
||||
const { type, link } = file.data;
|
||||
|
||||
if (link) {
|
||||
return <LinkPlaceholder ratio={ratio} />;
|
||||
}
|
||||
if (type.startsWith("video/")) {
|
||||
return <VideoPlaceholder ratio={ratio} />;
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ const truncateElements = (nbrOfLines) =>
|
||||
nbrOfLines &&
|
||||
css`
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: ${nbrOfLines};
|
||||
|
Loading…
Reference in New Issue
Block a user