From 12163de073be84c30c0b666ddc8d8a86d570148b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 13 Apr 2023 19:00:16 +0800 Subject: [PATCH] Show uploaded items in the upload modal --- packages/web/components/elements/Button.tsx | 8 + .../web/components/templates/UploadModal.tsx | 347 ++++++++++++++++++ .../templates/homeFeed/HomeFeedContainer.tsx | 247 +++---------- .../templates/homeFeed/LibraryFilterMenu.tsx | 86 +++-- .../mutations/uploadFileMutation.ts | 2 + packages/web/styles/globals.css | 16 +- 6 files changed, 469 insertions(+), 237 deletions(-) create mode 100644 packages/web/components/templates/UploadModal.tsx diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index a219854f7..1870ef9ce 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -159,6 +159,14 @@ export const Button = styled('button', { border: '1px solid $grayBorderHover', }, }, + link: { + color: '$grayText', + border: 'none', + bg: 'transparent', + '&:hover': { + opacity: 0.8, + }, + }, circularIcon: { mx: '$1', display: 'flex', diff --git a/packages/web/components/templates/UploadModal.tsx b/packages/web/components/templates/UploadModal.tsx new file mode 100644 index 000000000..3391c824d --- /dev/null +++ b/packages/web/components/templates/UploadModal.tsx @@ -0,0 +1,347 @@ +import { useRef, useCallback, useState } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' +import { + ModalContent, + ModalOverlay, + ModalRoot, + ModalTitleBar, +} from '../elements/ModalPrimitives' +import { styled } from '@stitches/react' +import Dropzone, { DropEvent, DropzoneRef, FileRejection } from 'react-dropzone' +import * as Progress from '@radix-ui/react-progress' +import { theme } from '../tokens/stitches.config' +import { uploadFileRequestMutation } from '../../lib/networking/mutations/uploadFileMutation' +import axios from 'axios' +import { CheckCircle, File } from 'phosphor-react' +import { showErrorToast } from '../../lib/toastHelpers' + +const DragnDropContainer = styled('div', { + width: '100%', + height: '80%', + position: 'absolute', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: '1', + alignSelf: 'center', + left: 0, + flexDirection: 'column', + padding: '25px', +}) + +const DragnDropStyle = styled('div', { + border: '1px solid $grayBorder', + borderRadius: '5px', + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + alignSelf: 'center', + color: '$thTextSubtle2', + padding: '10px', +}) + +const DragnDropIndicator = styled('div', { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + alignSelf: 'center', + width: '100%', + height: '100%', + borderRadius: '5px', +}) + +const ProgressIndicator = styled(Progress.Indicator, { + backgroundColor: '$omnivoreCtaYellow', + width: '100%', + height: '100%', +}) + +const ProgressRoot = styled(Progress.Root, { + position: 'relative', + overflow: 'hidden', + background: '$omnivoreGray', + borderRadius: '99999px', + width: '100%', + height: '5px', + transform: 'translateZ(0)', +}) + +type UploadModalProps = { + onOpenChange: (open: boolean) => void +} + +type UploadingFile = { + id: string + file: any + name: string + progress: number + status: 'inprogress' | 'success' | 'error' + openUrl: string | undefined +} + +export function UploadModal(props: UploadModalProps): JSX.Element { + const [uploadFiles, setUploadFiles] = useState([ + // { + // id: uuidv4(), + // file: '', + // name: 'test file', + // status: 'inprogress', + // progress: (371712 / 864476) * 100, + // openUrl: '', + // }, + ]) + const [inDragOperation, setInDragOperation] = useState(false) + const dropzoneRef = useRef(null) + + const openDialog = useCallback( + (event) => { + if (dropzoneRef.current) { + dropzoneRef.current.open() + } + event?.preventDefault() + }, + [dropzoneRef] + ) + + const handleAcceptedFiles = useCallback( + (acceptedFiles: any, event: DropEvent) => { + setInDragOperation(false) + + const addedFiles = acceptedFiles.map((file: { name: any }) => { + return { + id: uuidv4(), + file: file, + name: file.name, + percent: 0, + status: 'inprogress', + } + }) + + const allFiles = [...uploadFiles, ...addedFiles] + + setUploadFiles(allFiles) + ;(async () => { + for (const file of addedFiles) { + try { + const request = await uploadFileRequestMutation({ + // This will tell the backend not to save the URL + // and give it the local filename as the title. + url: `file://local/${file.id}/${file.file.path}`, + contentType: file.file.type, + createPageEntry: true, + }) + + if (!request?.uploadSignedUrl) { + showErrorToast('No upload URL available') + return + } + + const uploadResult = await axios.request({ + method: 'PUT', + url: request?.uploadSignedUrl, + data: file.file, + withCredentials: false, + headers: { + 'Content-Type': 'application/pdf', + }, + onUploadProgress: (p) => { + if (!p.total) { + console.warn('No total available for upload progress') + return + } + const progress = (p.loaded / p.total) * 100 + file.progress = progress + file.openUrl = `/article/sr/${request.createdPageId}` + + setUploadFiles([...allFiles]) + }, + }) + + file.status = 'success' + setUploadFiles([...allFiles]) + } catch (error) { + file.status = 'error' + setUploadFiles([...allFiles]) + } + } + })() + }, + [uploadFiles] + ) + + return ( + + + { + // remove focus from modal + ;(document.activeElement as HTMLElement).blur() + }} + > + + + { + setInDragOperation(true) + }} + onDragLeave={() => { + setInDragOperation(false) + }} + onDropAccepted={handleAcceptedFiles} + onDropRejected={( + fileRejections: FileRejection[], + event: DropEvent + ) => { + console.log('onDropRejected: ', fileRejections, event) + alert('You can only upload PDF files to your Omnivore Library.') + setInDragOperation(false) + event.preventDefault() + }} + preventDropOnDocument={true} + noClick={true} + accept={{ + 'application/pdf': ['.pdf'], + 'application/epub+zip': ['.epub'], + }} + > + {({ + getRootProps, + getInputProps, + acceptedFiles, + fileRejections, + }) => ( +
+ + + + + + {inDragOperation ? ( + <> + + Drop to upload your file + + + ) : ( + <> + + Drag files here to add them to your library + + + Or{' '} + + choose your files + + + + )} + + + + + {uploadFiles.map((file) => { + console.log('fileL ', file.name, file.progress) + return ( + + + {file.name} + + {file.progress == 100 ? ( + + {file.status == 'success' && ( + Read Now + )} + {file.status == 'error' && ( + + Error Uploading + + )} + + ) : ( + + {' '} + + )} + + ) + })} + + + +
+ )} +
+
+
+
+ ) +} diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index b1b8a2f4f..7ec6239ba 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -1,10 +1,8 @@ -import * as Progress from '@radix-ui/react-progress' import axios from 'axios' import { Action, createAction, useKBar, useRegisterActions } from 'kbar' import debounce from 'lodash/debounce' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import Dropzone from 'react-dropzone' import { Toaster } from 'react-hot-toast' import TopBarProgress from 'react-topbar-progress-indicator' import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll' @@ -17,7 +15,6 @@ import { } from '../../../lib/networking/fragments/articleFragment' import { Label } from '../../../lib/networking/fragments/labelFragment' import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' -import { uploadFileRequestMutation } from '../../../lib/networking/mutations/uploadFileMutation' import { SearchItem, TypeaheadSearchItemsData, @@ -37,7 +34,6 @@ import { StyledText } from '../../elements/StyledText' import { ConfirmationModal } from '../../patterns/ConfirmationModal' import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes' import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard' -import { styled, theme } from '../../tokens/stitches.config' import { SetLabelsModal } from '../article/SetLabelsModal' import { Box, HStack, VStack } from './../../elements/LayoutPrimitives' import { AddLinkModal } from './AddLinkModal' @@ -46,6 +42,7 @@ import { EmptyLibrary } from './EmptyLibrary' import { HighlightItemsLayout } from './HighlightsLayout' import { LibraryFilterMenu } from './LibraryFilterMenu' import { LibraryHeader } from './LibraryHeader' +import { UploadModal } from '../UploadModal' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' export type LibraryMode = 'reads' | 'highlights' @@ -78,8 +75,9 @@ export function HomeFeedContainer(): JSX.Element { const gridContainerRef = useRef(null) - const [labelsTarget, setLabelsTarget] = - useState(undefined) + const [labelsTarget, setLabelsTarget] = useState( + undefined + ) const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) @@ -611,33 +609,6 @@ type HomeFeedContentProps = { ) => Promise } -const DragnDropContainer = styled('div', { - width: '100%', - height: '80%', - position: 'absolute', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - zIndex: '1', - alignSelf: 'center', - left: 0, -}) - -const DragnDropStyle = styled('div', { - border: '3px dashed gray', - backgroundColor: 'aliceblue', - borderRadius: '5px', - width: '100%', - height: '100%', - opacity: '0.9', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - alignSelf: 'center', - left: 0, - margin: '16px', -}) - function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { const { viewerData } = useGetViewerQuery() const [layout, setLayout] = usePersistedState({ @@ -715,14 +686,11 @@ type LibraryItemsLayoutProps = { } & HomeFeedContentProps function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { - const [uploadingFiles, setUploadingFiles] = useState([]) - const [inDragOperation, setInDragOperation] = useState(false) - const [uploadProgress, setUploadProgress] = useState(0) - const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] = useState(false) const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] = useState(false) + const [showUploadModal, setShowUploadModal] = useState(true) const [, updateState] = useState({}) const removeItem = () => { @@ -744,52 +712,6 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { setShowUnsubscribeConfirmation(false) } - const handleDrop = async (acceptedFiles: any) => { - setInDragOperation(false) - setUploadingFiles(acceptedFiles.map((file: { name: any }) => file.name)) - - for (const file of acceptedFiles) { - try { - const request = await uploadFileRequestMutation({ - // This will tell the backend not to save the URL - // and give it the local filename as the title. - url: `file://local/${file.path}`, - contentType: file.type, - createPageEntry: true, - }) - if (!request?.uploadSignedUrl) { - throw 'No upload URL available' - } - - const uploadResult = await axios.request({ - method: 'PUT', - url: request?.uploadSignedUrl, - data: file, - withCredentials: false, - headers: { - 'Content-Type': 'application/pdf', - }, - onUploadProgress: (p) => { - if (!p.total) { - console.warn('No total available for upload progress') - return - } - const progress = (p.loaded / p.total) * 100 - console.log('upload progress: ', progress) - setUploadProgress(progress) - }, - }) - - console.log('result of uploading: ', uploadResult) - } catch (error) { - console.log('ERROR', error) - } - } - - setUploadingFiles([]) - props.reloadItems() - } - return ( <> {props.isValidating && props.items.length == 0 && } - - { - setInDragOperation(true) - }} - onDragLeave={() => { - setInDragOperation(false) - }} - preventDropOnDocument={true} - noClick={true} - accept={{ - 'application/pdf': ['.pdf'], +
{ + setShowUploadModal(true) }} + style={{ height: '100%', width: '100%' }} > - {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => ( -
- {inDragOperation && uploadingFiles.length < 1 && ( - - - - Drop PDF document to to upload and add to your library - - - - )} - {uploadingFiles.length > 0 && ( - - - - - - - - Uploading file - - - - - )} - - {!props.isValidating && props.items.length == 0 ? ( - { - props.setShowAddLinkModal(true) - }} - /> - ) : ( - - )} - - {props.hasMore ? ( - - ) : ( - - )} - -
+ {!props.isValidating && props.items.length == 0 ? ( + { + props.setShowAddLinkModal(true) + }} + /> + ) : ( + )} - + + {props.hasMore ? ( + + ) : ( + + )} + +
{props.showEditTitleModal && ( )} + {showUploadModal && ( + setShowUploadModal(false)} /> + )} ) } diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx index d6dc48581..82e611d7b 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -3,7 +3,7 @@ import { StyledText } from '../../elements/StyledText' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' import { Button } from '../../elements/Button' -import { CaretRight, Circle, DotsThree, Plus } from 'phosphor-react' +import { CaretRight, Circle, DotsThree, Plus, Upload } from 'phosphor-react' import { useGetSubscriptionsQuery } from '../../../lib/networking/queries/useGetSubscriptionsQuery' import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' import { Label } from '../../../lib/networking/fragments/labelFragment' @@ -444,33 +444,65 @@ function AddLinkButton(props: AddLinkButtonProps): JSX.Element { }} distribution="center" > - + color: isDark + ? theme.colors.thHighContrast.toString() + : theme.colors.thTextContrast2.toString(), + display: 'flex', + alignItems: 'center', + fontWeight: '600', + bg: isDark + ? 'transparent' + : theme.colors.thBackground2.toString(), + border: `1px solid ${ + isDark ? theme.colors.thHighContrast.toString() : 'transparent' + }`, + }} + onClick={(e) => { + props.showAddLinkModal() + e.preventDefault() + }} + > + + Add Link + + + ) diff --git a/packages/web/lib/networking/mutations/uploadFileMutation.ts b/packages/web/lib/networking/mutations/uploadFileMutation.ts index 85ce6f020..40ed75616 100644 --- a/packages/web/lib/networking/mutations/uploadFileMutation.ts +++ b/packages/web/lib/networking/mutations/uploadFileMutation.ts @@ -16,6 +16,7 @@ type UploadFileResponseData = { type UploadFileData = { id: string uploadSignedUrl: string + createdPageId: string } export async function uploadFileRequestMutation( @@ -30,6 +31,7 @@ export async function uploadFileRequestMutation( ... on UploadFileRequestSuccess { id uploadSignedUrl + createdPageId } } }` diff --git a/packages/web/styles/globals.css b/packages/web/styles/globals.css index 79d19363d..eec43638a 100644 --- a/packages/web/styles/globals.css +++ b/packages/web/styles/globals.css @@ -375,6 +375,7 @@ div#appleid-signin { select { color: var(--colors-grayTextContrast); background-color: transparent; + -webkit-appearance: none; } select option { @@ -412,18 +413,3 @@ button { background: grey; margin-bottom: 16px; } */ - -.ProgressRoot { - overflow: hidden; - background: var(--colors-omnivoreGray); - border-radius: 99999px; - height: 25px; - transform: translateZ(0); -} - -.ProgressIndicator { - background-color: var(--colors-omnivoreCtaYellow) ; - width: 100%; - height: 100%; - transition: transform 660ms cubic-bezier(0.65, 0, 0.35, 1); -} \ No newline at end of file