diff --git a/components/system/components/GlobalCarousel/index.js b/components/system/components/GlobalCarousel/index.js new file mode 100644 index 00000000..43da8410 --- /dev/null +++ b/components/system/components/GlobalCarousel/index.js @@ -0,0 +1,792 @@ +import * as React from "react"; +import * as Constants from "~/common/constants"; +import * as SVG from "~/common/svg"; +import * as System from "~/components/system"; +import * as Styles from "~/common/styles"; +import * as Jumpers from "~/components/system/components/GlobalCarousel/jumpers"; + +import { css } from "@emotion/react"; +import { Alert } from "~/components/core/Alert"; +import { motion } from "framer-motion"; +import { + useDetectTextOverflow, + useEscapeKey, + useEventListener, + useLockScroll, +} from "~/common/hooks"; +import { Show } from "~/components/utility/Show"; +import { ModalPortal } from "~/components/core/ModalPortal"; + +import SlateMediaObject from "~/components/core/SlateMediaObject"; +import LinkIcon from "~/components/core/LinkIcon"; + +/* ------------------------------------------------------------------------------------------------- + * Carousel Header + * -----------------------------------------------------------------------------------------------*/ + +const VisitLinkButton = ({ file }) => { + return ( + e.stopPropagation()} + style={{ + color: Constants.semantic.textGrayDark, + padding: "4px 8px 7px", + marginLeft: 4, + minHeight: 30, + }} + href={file.url} + target="_blank" + rel="noreferrer" + type="link" + > + + Visit site + + ); +}; + +/* -----------------------------------------------------------------------------------------------*/ + +const useCarouselJumperControls = () => { + const [isControlVisible, setControlVisibility] = React.useState(false); + const showControl = () => setControlVisibility(true); + const hideControl = () => setControlVisibility(false); + return [isControlVisible, { showControl, hideControl }]; +}; + +const STYLES_HEADER_WRAPPER = (theme) => css` + ${Styles.HORIZONTAL_CONTAINER_CENTERED}; + position: absolute; + width: 100%; + min-height: 64px; + padding: 13px 24px 10px; + color: ${theme.semantic.textGrayDark}; + border-bottom: 1px solid ${theme.semantic.borderGrayLight}; + box-shadow: ${theme.shadow.lightSmall}; + z-index: 1; + + background-color: ${theme.semantic.bgWhite}; + @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) { + background-color: ${theme.semantic.bgBlurWhiteOP}; + -webkit-backdrop-filter: blur(15px); + backdrop-filter: blur(15px); + } +`; + +const STYLES_ACTION_BUTTON = css` + ${Styles.BUTTON_RESET}; + height: 32px; + width: 32px; +`; + +function CarouselHeader({ + viewer, + data, + external, + isOwner, + file, + current, + total, + onAction, + onClose, + ...props +}) { + const [isHeaderVisible, setHeaderVisibility] = React.useState(true); + const timeoutRef = React.useRef(); + + const showHeader = () => { + clearTimeout(timeoutRef.current); + setHeaderVisibility(true); + }; + + const hideHeader = () => { + timeoutRef.current = setTimeout(() => { + setHeaderVisibility(false); + }, 500); + }; + + React.useEffect(() => { + timeoutRef.current = setTimeout(hideHeader, 3000); + return () => clearTimeout(timeoutRef.current); + }, []); + + // NOTE(amine): Detect if the text is overflowing to show the MORE button + const elementRef = React.useRef(); + const isBodyOverflowing = useDetectTextOverflow({ ref: elementRef }, [file]); + + // NOTE(amine): jumpers handlers + const [ + isFileDescriptionVisible, + { showControl: showFileDescription, hideControl: hideFileDescription }, + ] = useCarouselJumperControls(); + + const [isMoreInfoVisible, { showControl: showMoreInfo, hideControl: hideMoreInfo }] = + useCarouselJumperControls(); + + const [isEditInfoVisible, { showControl: showEditInfo, hideControl: hideEditInfo }] = + useCarouselJumperControls(); + + const [isShareFileVisible, { showControl: showShareFile, hideControl: hideShareFile }] = + useCarouselJumperControls(); + + const [isEditChannelsVisible, { showControl: showEditChannels, hideControl: hideEditChannels }] = + useCarouselJumperControls(); + + const isJumperOpen = + isFileDescriptionVisible || + isMoreInfoVisible || + isEditInfoVisible || + isShareFileVisible || + isEditChannelsVisible; + + return ( + <> + + {isOwner && ( + + )} + {isOwner && ( + + )} + + + + + + +
+
+ + {file?.name || file?.filename} + + + {current} / {total} + +
+ +
+ + {file.body} + + + + MORE + + +
+
+
+
+ + + + + + + + + {file.isLink ? : null} +
+
+ +
+
+
+ + ); +} + +const STYLES_CAROUSEL_MOBILE_HEADER = (theme) => css` + position: relative; + ${Styles.HORIZONTAL_CONTAINER_CENTERED}; + padding: 7px 8px 3px; + color: ${theme.semantic.textGrayDark}; + border-bottom: 1px solid ${theme.semantic.borderGrayLight}; + + background-color: ${theme.semantic.bgWhite}; + @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) { + background-color: ${theme.semantic.bgBlurWhiteOP}; + -webkit-backdrop-filter: blur(15px); + backdrop-filter: blur(15px); + } +`; + +const STYLES_CAROUSEL_MOBILE_FOOTER = (theme) => css` + ${Styles.HORIZONTAL_CONTAINER_CENTERED}; + justify-content: space-between; + z-index: 1; + width: 100%; + padding: 8px 16px; + border-top: 1px solid ${theme.semantic.borderGrayLight}; + color: ${theme.semantic.textGrayDark}; + + background-color: ${theme.semantic.bgWhite}; + @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) { + background-color: ${theme.semantic.bgBlurWhite}; + -webkit-backdrop-filter: blur(15px); + backdrop-filter: blur(15px); + } +`; + +const STYLES_CAROUSEL_MOBILE_SLIDE_COUNT = css` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +function CarouselHeaderMobile({ current, total, onClose, onNextSlide, onPreviousSlide }) { + return ( + + ); +} + +function CarouselFooterMobile({ file, onAction, external, isOwner, data, viewer }) { + const [isEditInfoVisible, { showControl: showEditInfo, hideControl: hideEditInfo }] = + useCarouselJumperControls(); + + const [isShareFileVisible, { showControl: showShareFile, hideControl: hideShareFile }] = + useCarouselJumperControls(); + + const [isMoreInfoVisible, { showControl: showMoreInfo, hideControl: hideMoreInfo }] = + useCarouselJumperControls(); + + const [isEditChannelsVisible, { showControl: showEditChannels, hideControl: hideEditChannels }] = + useCarouselJumperControls(); + return ( + <> + + {isOwner && ( + + )} + {isOwner && ( + + )} + + + + + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Carousel Controls + * -----------------------------------------------------------------------------------------------*/ + +const useCarouselKeyCommands = ({ handleNext, handlePrevious, handleClose }) => { + const handleKeyDown = (e) => { + const inputs = document.querySelectorAll("input"); + for (let elem of inputs) { + if (document.activeElement === elem) { + return; + } + } + + const textareas = document.querySelectorAll("textarea"); + for (let elem of textareas) { + if (document.activeElement === elem) { + return; + } + } + + switch (e.key) { + case "Right": + case "ArrowRight": + handleNext(); + break; + case "Left": + case "ArrowLeft": + handlePrevious(); + break; + } + }; + + useEscapeKey(handleClose); + + useEventListener({ type: "keydown", handler: handleKeyDown }); +}; + +const STYLES_CONTROLS_BUTTON = (theme) => css` + ${Styles.BUTTON_RESET}; + background-color: ${theme.semantic.bgGrayLight}; + border-radius: 8px; + border: 1px solid ${theme.semantic.borderGrayLight}; + padding: 10px; + box-shadow: ${theme.shadow.lightMedium}; + svg { + display: block; + } +`; + +const STYLES_CONTROLS_WRAPPER = css` + ${Styles.CONTAINER_CENTERED}; + position: absolute; + width: 122px; + height: 100%; + z-index: 1; + top: 0; + padding-left: 24px; + padding-right: 24px; +`; + +function CarouselControls({ + enableNextSlide, + enablePreviousSlide, + onNextSlide, + onPreviousSlide, + onClose, +}) { + useCarouselKeyCommands({ + handleNext: onNextSlide, + handlePrevious: onPreviousSlide, + handleClose: onClose, + }); + + const [areControlsVisible, setControlsVisibility] = React.useState(true); + const timeoutRef = React.useRef(); + + const showControls = () => { + clearTimeout(timeoutRef.current); + setControlsVisibility(true); + }; + + const hideControls = () => { + timeoutRef.current = setTimeout(() => { + setControlsVisibility(false); + }, 500); + }; + + React.useEffect(() => { + timeoutRef.current = setTimeout(hideControls, 3000); + return () => clearTimeout(timeoutRef.current); + }, []); + + return ( + <> +
+ {enablePreviousSlide ? ( + + + + ) : null} +
+
+ {enableNextSlide ? ( + + + + ) : null} +
+ + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Carousel Content + * -----------------------------------------------------------------------------------------------*/ + +const STYLES_CONTENT = (theme) => css` + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + @media (max-width: ${theme.sizes.mobile}px) { + flex-direction: column; + overflow-y: auto; + justify-content: flex-start; + } +`; + +const STYLES_PREVIEW_WRAPPER = (theme) => css` + ${Styles.CONTAINER_CENTERED}; + position: relative; + width: 100%; + height: 100%; + + @media (max-width: ${theme.sizes.mobile}px) { + min-height: 75vh; + height: 75vh; + overflow: hidden; + } +`; + +export function CarouselContent({ + carouselType, + objects, + index, + data, + isMobile, + viewer, + sidebar, + style, + onClose, +}) { + const file = objects?.[index]; + + let isRepost = false; + if (carouselType === "SLATE") isRepost = data?.ownerId !== file.ownerId; + + useLockScroll(); + + return ( + <> + +
+
+ +
+ +
+ + {file?.name || file?.filename} + + + + {file?.body} + + + +
+ + + {file.url} + +
+
+
+
+ + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Global Carousel + * -----------------------------------------------------------------------------------------------*/ +const useCarouselViaParams = ({ index, params, objects, onChange }) => { + const findSelectedIndex = () => { + const cid = params?.cid; + if (!cid) return -1; + + let index = objects.findIndex((elem) => elem.cid === cid); + return index; + }; + + React.useEffect(() => { + if (index !== -1) return; + + const selectedIndex = findSelectedIndex(); + if (selectedIndex !== index) onChange(index); + }, [params?.cid]); + + React.useEffect(() => { + if (params?.cid) { + const index = findSelectedIndex(); + onChange(index); + } + }, [params]); +}; + +const getCarouselHandlers = ({ index, objects, params, onChange, onAction }) => { + const handleNext = (e) => { + if (e) e.stopPropagation(); + + let nextIndex = index + 1; + if (nextIndex >= objects.length) return; + + let { cid } = objects[nextIndex]; + onChange(nextIndex); + onAction({ type: "UPDATE_PARAMS", params: { ...params, cid }, redirect: true }); + }; + + const handlePrevious = (e) => { + if (e) e.stopPropagation(); + + let prevIndex = index - 1; + if (prevIndex < 0) return; + + let { cid } = objects[prevIndex]; + onChange(prevIndex); + onAction({ type: "UPDATE_PARAMS", params: { params, cid }, redirect: true }); + }; + + const handleClose = (e) => { + if (e) e.stopPropagation(), e.preventDefault(); + + let params = { ...params }; + delete params.cid; + onAction({ type: "UPDATE_PARAMS", params, redirect: true }); + onChange(-1); + }; + return { handleNext, handlePrevious, handleClose }; +}; + +const STYLES_ROOT = (theme) => css` + ${Styles.VERTICAL_CONTAINER}; + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + height: 100vh; + color: ${Constants.system.white}; + z-index: ${Constants.zindex.modal}; + background-color: rgba(0, 0, 0, 0.8); + + // Note(Amine): we're using the blur filter to fix a weird backdrop-filter's bug in chrome + + filter: blur(0px); + @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) { + -webkit-backdrop-filter: blur(15px); + backdrop-filter: blur(15px); + } + + @media (max-width: ${Constants.sizes.mobile}px) { + background-color: ${theme.semantic.bgWhite}; + } + + @keyframes global-carousel-fade-in { + from { + transform: translateX(8px); + opacity: 0; + } + to { + transform: translateX(0px); + opacity: 1; + } + } + animation: global-carousel-fade-in 400ms ease; +`; + +export function GlobalCarousel({ + carouselType, + objects, + index, + params, + data, + isMobile, + onChange, + onAction, + viewer, + external, + isOwner, + sidebar, + style, +}) { + const file = objects?.[index]; + const isCarouselOpen = (carouselType || index > 0 || index <= objects.length) && !!file; + + useCarouselViaParams({ index, params, objects, onChange }); + + if (!isCarouselOpen) return null; + + const { handleNext, handlePrevious, handleClose } = getCarouselHandlers({ + index, + objects, + params, + onChange, + onAction, + }); + + return ( +
+ {!isMobile ? ( + 0} + onNextSlide={handleNext} + onPreviousSlide={handlePrevious} + onClose={handleClose} + /> + ) : null} + + {isMobile ? ( + + ) : ( + + )} + + + {isMobile ? ( + + ) : null} +
+ ); +}