diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 2869846d1..9c53341a6 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -1,4 +1,6 @@ -import { Box, HStack, VStack } from './../../elements/LayoutPrimitives' +import { Box, HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives' +import Dropzone from 'react-dropzone' +import * as Progress from '@radix-ui/react-progress' import type { LibraryItem, LibraryItemsQueryInput, @@ -13,7 +15,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { LibrarySearchBar } from './LibrarySearchBar' import { StyledText } from '../../elements/StyledText' import { AddLinkModal } from './AddLinkModal' -import { styled } from '../../tokens/stitches.config' +import { styled, theme } from '../../tokens/stitches.config' import { ListLayoutIcon } from '../../elements/images/ListLayoutIcon' import { GridLayoutIcon } from '../../elements/images/GridLayoutIcon' import { @@ -50,6 +52,8 @@ import { TypeaheadSearchItemsData, typeaheadSearchQuery, } from '../../../lib/networking/queries/typeaheadSearch' +import axios from 'axios' +import { uploadFileRequestMutation } from '../../../lib/networking/mutations/uploadFileMutation' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' @@ -125,8 +129,14 @@ export function HomeFeedContainer(): JSX.Element { }) ) - const { itemsPages, size, setSize, isValidating, performActionOnItem } = - useGetLibraryItemsQuery(queryInputs) + const { + itemsPages, + size, + setSize, + isValidating, + performActionOnItem, + mutate, + } = useGetLibraryItemsQuery(queryInputs) useEffect(() => { if (queryValue.startsWith('#')) { @@ -558,6 +568,7 @@ export function HomeFeedContainer(): JSX.Element { { @@ -611,6 +622,7 @@ export function HomeFeedContainer(): JSX.Element { type HomeFeedContentProps = { items: LibraryItem[] searchTerm?: string + reloadItems: () => void gridContainerRef: React.RefObject applySearchQuery: (searchQuery: string) => void hasMore: boolean @@ -683,6 +695,20 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { }, }) + const DragnDropStyle = styled('div', { + border: '3px dashed gray', + backgroundColor: 'aliceblue', + borderRadius: '5px', + width: '95%', + height: '80%', + position: 'absolute', + opacity: '0.9', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: '1', + }) + const removeItem = () => { if (!props.linkToRemove) { return @@ -702,6 +728,51 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { setShowUnsubscribeConfirmation(false) } + const [uploadingFiles, setUploadingFiles] = useState([]) + const [inDragOperation, setInDragOperation] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + + 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) => { + console.log('upload progress: ', (p.loaded / p.total) * 100) + setUploadProgress((p.loaded / p.total) * 100) + }, + }) + + console.log('result of uploading: ', uploadResult) + } catch (error) { + console.log('ERROR', error) + } + } + + setUploadingFiles([]) + props.reloadItems() + } + return ( <> + {props.isValidating && props.items.length == 0 && } + {viewerData?.me && ( )} - {!props.isValidating && props.items.length == 0 ? ( - { - props.setShowAddLinkModal(true) - }} - /> - ) : ( - - {props.items.map((linkedItem) => ( - div': { - bg: '$grayBg', - }, - '&:focus': { - '> div': { - bg: '$grayBgActive', - }, - }, - '&:hover': { - '> div': { - bg: '$grayBgActive', - }, - }, - }} - > - {viewerData?.me && ( - { - if (action === 'delete') { - setShowRemoveLinkConfirmation(true) - props.setLinkToRemove(linkedItem) - } else if (action === 'editTitle') { - props.setShowEditTitleModal(true) - props.setLinkToEdit(linkedItem) - } else if (action == 'unsubscribe') { - setShowUnsubscribeConfirmation(true) - props.setLinkToUnsubscribe(linkedItem) - } else { - props.actionHandler(action, linkedItem) - } - }} - /> - )} - - ))} - - )} - { + setInDragOperation(true) + }} + onDragLeave={() => { + setInDragOperation(false) + }} + preventDropOnDocument={true} + noClick={true} > - {props.hasMore ? ( - - ) : ( - + {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => ( +
+ {inDragOperation && uploadingFiles.length < 1 && ( + + + Drop PDF document here to add to your library + + + )} + {uploadingFiles.length > 0 && ( + + + + + + + Uploading file + + + + )} + + {!props.isValidating && props.items.length == 0 ? ( + { + props.setShowAddLinkModal(true) + }} + /> + ) : ( + + {props.items.map((linkedItem) => ( + div': { + bg: '$grayBg', + }, + '&:focus': { + '> div': { + bg: '$grayBgActive', + }, + }, + '&:hover': { + '> div': { + bg: '$grayBgActive', + }, + }, + }} + > + {viewerData?.me && ( + { + if (action === 'delete') { + setShowRemoveLinkConfirmation(true) + props.setLinkToRemove(linkedItem) + } else if (action === 'editTitle') { + props.setShowEditTitleModal(true) + props.setLinkToEdit(linkedItem) + } else if (action == 'unsubscribe') { + setShowUnsubscribeConfirmation(true) + props.setLinkToUnsubscribe(linkedItem) + } else { + props.actionHandler(action, linkedItem) + } + }} + /> + )} + + ))} + + )} + + {props.hasMore ? ( + + ) : ( + + )} + +
)} -
+
+ {/* Temporary code */} + {/*
+ Files: +
    + {uploadingFiles.map((fileName) => ( +
  • {fileName}
  • + ))} +
+
*/} + {/* Temporary code */} {props.showAddLinkModal && ( props.setShowAddLinkModal(false)} /> )} diff --git a/packages/web/components/templates/library/LibraryList.tsx b/packages/web/components/templates/library/LibraryList.tsx index d7ad55f3c..b701c34b5 100644 --- a/packages/web/components/templates/library/LibraryList.tsx +++ b/packages/web/components/templates/library/LibraryList.tsx @@ -1,7 +1,8 @@ +import { useMemo, useState } from 'react' +import Dropzone from 'react-dropzone' import { Box } from '../../elements/LayoutPrimitives' import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery' import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences' -import { useMemo } from 'react' import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes' import { LibraryGridCard } from '../../patterns/LibraryCards/LibraryGridCard' @@ -9,7 +10,6 @@ import { LayoutCoordinator } from './LibraryContainer' import { EmptyLibrary } from '../homeFeed/EmptyLibrary' import Masonry from 'react-masonry-css' - export type LibraryListProps = { layoutCoordinator: LayoutCoordinator } @@ -28,6 +28,15 @@ export function LibraryList(props: LibraryListProps): JSX.Element { const { itemsPages, size, setSize, isValidating, performActionOnItem } = useGetLibraryItemsQuery(defaultQuery) + const [fileNames, setFileNames] = useState([]) + const [inDragOperation, setInDragOperation] = useState(false) + const [uploadingFiles, setUploadingFiles] = useState([]) + + const handleDrop = (acceptedFiles: any) => { + setFileNames(acceptedFiles.map((file: { name: any }) => file.name)) + setUploadingFiles(acceptedFiles.map((file: { name: any }) => file.name)) + } + const libraryItems = useMemo(() => { const items = itemsPages?.flatMap((ad) => { @@ -45,57 +54,116 @@ export function LibraryList(props: LibraryListProps): JSX.Element { /> ) } + console.log(fileNames) return ( - - {libraryItems.map((linkedItem) => ( + {inDragOperation && uploadingFiles.length < 1 && ( + div': { - bg: '$libraryBackground', - }, - '&:focus': { - '> div': { - bg: '$grayBgActive', - }, - }, - '&:hover': { - '> div': { - bg: '$grayBgActive', - }, - }, + color: '$utilityTextDefault', + fontWeight: '800', + fontSize: '$4', }} > - {viewerData?.me && ( - { - console.log('card clicked') - }} - /> - )} + Drag n drop files here - ))} - + + )} + { + setInDragOperation(true) + }} + onDragLeave={() => { + setInDragOperation(false) + }} + noClick={true} + noDragEventsBubbling={true} + > + {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => ( + + + + {libraryItems.map((linkedItem) => ( + div': { + bg: '$libraryBackground', + }, + '&:focus': { + '> div': { + bg: '$grayBgActive', + }, + }, + '&:hover': { + '> div': { + bg: '$grayBgActive', + }, + }, + }} + > + {viewerData?.me && ( + { + console.log('card clicked') + }} + /> + )} + + ))} + + + )} + + {/* Temporary code */} +
+ Files: +
    + {fileNames.map((fileName) => ( +
  • {fileName}
  • + ))} +
+
{' '} + {/* Temporary code */} {/* Extra padding at bottom to give space for scrolling */} ) -} \ No newline at end of file +} diff --git a/packages/web/lib/networking/mutations/uploadFileMutation.ts b/packages/web/lib/networking/mutations/uploadFileMutation.ts new file mode 100644 index 000000000..17625fc9c --- /dev/null +++ b/packages/web/lib/networking/mutations/uploadFileMutation.ts @@ -0,0 +1,55 @@ +import { gqlFetcher } from '../networkHelpers' +import { v4 as uuidv4 } from 'uuid' + + +type UploadFileInput = { + url: string + contentType: string + createPageEntry?: boolean + clientRequestId?: string +} + +type UploadFileOutput = { + jobId?: string + url?: string + clientRequestId?: string +} + +type UploadFileResponseData = { + uploadFileRequest?: UploadFileData + errorCodes?: unknown[] +} + +type UploadFileData = { + id: string + uploadSignedUrl: string +} + +export async function uploadFileRequestMutation( + input: UploadFileInput +): Promise { + const mutation = ` + mutation UploadFileRequest($input: UploadFileRequestInput!) { + uploadFileRequest(input:$input) { + ... on UploadFileRequestError { + errorCodes + } + ... on UploadFileRequestSuccess { + id + uploadSignedUrl + } + } + }` + + if (!input.clientRequestId) { + input.clientRequestId = uuidv4() + } + + const data = await gqlFetcher(mutation, { input }) + const output = data as UploadFileResponseData | undefined + const error = output?.errorCodes?.find(() => true) + if (error) { + throw error + } + return output?.uploadFileRequest +} diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index b729bebc5..001842abe 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -27,6 +27,7 @@ type LibraryItemsQueryResponse = { size: number | ((_size: number) => number) ) => Promise performActionOnItem: (action: LibraryItemAction, item: LibraryItem) => void + mutate: () => void } type LibraryItemAction = @@ -352,5 +353,6 @@ export function useGetLibraryItemsQuery({ performActionOnItem, size, setSize, + mutate } } diff --git a/packages/web/package.json b/packages/web/package.json index 3610ecbf3..6c47538d0 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-dropdown-menu": "^0.1.6", "@radix-ui/react-id": "^0.1.1", "@radix-ui/react-popover": "^0.1.1", + "@radix-ui/react-progress": "^1.0.1", "@radix-ui/react-separator": "^0.1.0", "@radix-ui/react-tooltip": "^0.1.7", "@segment/analytics-next": "^1.33.5", @@ -43,6 +44,7 @@ "react-apple-login": "^1.1.3", "react-colorful": "^5.5.1", "react-dom": "^17.0.2", + "react-dropzone": "^14.2.3", "react-hot-toast": "^2.1.1", "react-masonry-css": "^1.0.16", "react-pro-sidebar": "^0.7.1", diff --git a/packages/web/styles/globals.css b/packages/web/styles/globals.css index 2b11e4ad9..f8616bd79 100644 --- a/packages/web/styles/globals.css +++ b/packages/web/styles/globals.css @@ -376,4 +376,19 @@ button { /* .omnivore-masonry-grid_column > div { background: grey; margin-bottom: 16px; -} */ \ No newline at end of file +} */ + +.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 diff --git a/yarn.lock b/yarn.lock index b3c11ece1..b98128878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5172,6 +5172,11 @@ dependencies: "@babel/runtime" "^7.13.10" +"@ramonak/react-progress-bar@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@ramonak/react-progress-bar/-/react-progress-bar-5.0.3.tgz#a3518fb19e6650e593a208dd429dca7cb6b63d52" + integrity sha512-VxXGKN74q94jYoeYuFNJm3xvWhVz9dy+alFZ8S4ZmTTr/05CCq9PjwthT8JB27UdAvn8pHvKBmemV8JU2cZi6A== + "@reach/observe-rect@^1.1.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" @@ -9907,6 +9912,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +attr-accept@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + auto-bind@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" @@ -14178,6 +14188,13 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-selector@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== + dependencies: + tslib "^2.4.0" + file-system-cache@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f" @@ -21650,7 +21667,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.0.0, prop-types@^15.6.0: +prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -22206,6 +22223,15 @@ react-draggable@^4.4.3: clsx "^1.1.1" prop-types "^15.6.0" +react-dropzone@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" + integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== + dependencies: + attr-accept "^2.2.2" + file-selector "^0.6.0" + prop-types "^15.8.1" + react-element-to-jsx-string@^14.3.4: version "14.3.4" resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8"