import * as React from "react"; import * as Styles from "~/common/styles"; import * as System from "~/components/system"; import * as Constants from "~/common/constants"; import * as SVG from "~/common/svg"; import { useUploadContext } from "~/components/core/Upload/Provider"; import { motion, AnimatePresence } from "framer-motion"; import { css } from "@emotion/react"; import { Match, Switch } from "~/components/utility/Switch"; import { Show } from "~/components/utility/Show"; import { useHover } from "~/common/hooks"; import DataMeter from "~/components/core/DataMeter"; import BlobObjectPreview from "~/components/core/BlobObjectPreview"; /* ------------------------------------------------------------------------------------------------- * Popup * -----------------------------------------------------------------------------------------------*/ const STYLES_POPUP_WRAPPER = (theme) => css` position: fixed; bottom: 24px; right: 24px; z-index: ${theme.zindex.sidebar}; @media (max-width: ${theme.sizes.mobile}px) { right: 50%; transform: translateX(50%); } `; const STYLES_DISMISS_BUTTON = (theme) => css` ${Styles.BUTTON_RESET}; position: absolute; right: -8px; top: -8px; border-radius: 50%; padding: 4px; border: 1px solid ${theme.semantic.borderGrayLight4}; color: ${theme.semantic.textGrayDark}; background-color: ${theme.semantic.bgWhite}; @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); background-color: ${theme.semantic.bgBlurWhiteOP}; } svg { display: block; } `; const STYLES_POPUP_CONTENT = css` border-radius: 12px; overflow: hidden; `; const useUploadPopup = ({ totalFilesSummary }) => { const [{ isFinished }, { resetUploadState }] = useUploadContext(); const [popupState, setPopupState] = React.useState({ isVisible: false, isSummaryExpanded: false, }); // NOTE(amine): popup handlers const showUploadPopup = () => setPopupState((prev) => ({ ...prev, isVisible: true })); const hideUploadPopup = () => setPopupState((prev) => ({ ...prev, isVisible: false })); const expandUploadSummary = () => setPopupState({ isVisible: true, isSummaryExpanded: true }); const collapseUploadSummary = () => setPopupState({ isVisible: true, isSummaryExpanded: false }); const timeoutRef = React.useRef(); //NOTE(amine): show the upload summary, then automatically collapse the upload summary after 3 seconds const isStarted = totalFilesSummary.total > 0; React.useEffect(() => { if (!isStarted) return; expandUploadSummary(); timeoutRef.current = setTimeout(collapseUploadSummary, 3000); }, [isStarted]); /** * NOTE(amine): show the upload summary when a file fails to upload, * then automatically collapse the upload summary after 3 seconds */ const isSummaryExpandedRef = React.useRef(); isSummaryExpandedRef.current = popupState.isSummaryExpanded; React.useEffect(() => { if (isSummaryExpandedRef.current || totalFilesSummary.failed === 0) return; expandUploadSummary(); clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(collapseUploadSummary, 3000); }, [totalFilesSummary.failed]); // NOTE(amine): show the upload summary when upload finishes const totalFilesSummaryRef = React.useRef(); totalFilesSummaryRef.current = totalFilesSummary; React.useEffect(() => { if (!isFinished) return; //NOTE(amine): if all the upload items have been canceled, hide the upload popup if (totalFilesSummaryRef.current.total === 0) { hideUploadPopup(); resetUploadState(); return; } clearTimeout(timeoutRef.current); expandUploadSummary(); }, [isFinished]); /** * NOTE(amine): the upload summary is set to automatically collapse when the upload starts and when a file fails to upload. * Let's cancel those effects when the user hovers over the summary */ const cancelAutoCollapseOnMouseEnter = () => clearTimeout(timeoutRef.current); return [ popupState, { showUploadPopup, hideUploadPopup, expandUploadSummary, collapseUploadSummary, cancelAutoCollapseOnMouseEnter, }, ]; }; const useUploadSummary = ({ fileLoading }) => React.useMemo(() => { let totalFilesSummary = { failed: 0, duplicate: 0, saved: 0, total: 0 }; const uploadSummary = Object.entries(fileLoading).map(([, file]) => { totalFilesSummary["total"]++; if (file.status === "saving") return { ...file, filename: file.name }; totalFilesSummary[file.status]++; return { ...file, filename: file.name }; }); const statusOrder = { failed: 1, saving: 2, duplicate: 3, saved: 4, }; return { totalFilesSummary, uploadSummary: uploadSummary.sort( (a, b) => statusOrder[a.status] - statusOrder[b.status] || a.createdAt - b.createdAt ), }; }, [fileLoading]); export function Popup() { const [{ isFinished, fileLoading }, { resetUploadState }] = useUploadContext(); const { uploadSummary, totalFilesSummary } = useUploadSummary({ fileLoading }); const [isHovered, { handleOnMouseEnter, handleOnMouseLeave }] = useHover(); const [ popupState, { hideUploadPopup, expandUploadSummary, collapseUploadSummary, cancelAutoCollapseOnMouseEnter }, ] = useUploadPopup({ totalFilesSummary }); if (!popupState.isVisible) return null; return (
{popupState.isSummaryExpanded ? ( ) : null}
); } /* ------------------------------------------------------------------------------------------------- * Popup Header * -----------------------------------------------------------------------------------------------*/ const STYLES_POPUP_HEADER = (theme) => css` color: ${theme.semantic.textGrayDark}; width: 264px; padding: 9px 12px 7px; background-color: ${theme.semantic.bgWhite}; @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); background-color: ${theme.semantic.bgBlurWhiteOP}; } border-radius: 12px; transition: border-radius 0.5s ease-in-out; border: 1px solid ${theme.semantic.bgGrayLight}; svg { display: block; } `; const STYLES_RESET_BORDER_TOP = css` border-top: none; border-radius: 0px 0px 12px 12px; `; function Header({ totalFilesSummary, popupState, expandUploadSummary, collapseUploadSummary }) { const [{ isFinished, totalBytesUploaded, totalBytes }, { retryAll }] = useUploadContext(); const uploadProgress = Math.floor((totalBytesUploaded / totalBytes) * 100); if (isFinished && totalFilesSummary.failed > 0) { return ( {totalFilesSummary.failed} failed ); } if (isFinished) { return (
{totalFilesSummary.saved + totalFilesSummary.duplicate} saved
); } return ( {uploadProgress}% ); } /* ------------------------------------------------------------------------------------------------- * Popup Summary * -----------------------------------------------------------------------------------------------*/ const STYLES_SUMMARY = (theme) => css` position: relative; background-color: ${theme.system.white}; max-height: 312px; overflow-y: auto; overflow-x: hidden; border: 1px solid ${theme.semantic.bgGrayLight}; border-bottom: none; border-radius: 12px 12px 0px 0px; // NOTE(amine): fix alignment issue caused by inline display svg { display: block; } `; const STYLES_PREVIEW_WRAPPER = css` width: 24px; height: 24px; border-radius: 6px; overflow: hidden; `; const STYLES_SUMMARY_ACTION = css` ${Styles.BUTTON_RESET}; position: relative; width: 32; height: 32; right: -16; `; function Summary({ uploadSummary }) { const [, { retry, cancel }] = useUploadContext(); return (
{uploadSummary.map((file) => ( <>
{file.name} Saved}> failed already saved
}>
))}
); }