import * as Progress from '@radix-ui/react-progress' import { styled } from '@stitches/react' import axios from 'axios' import { File } from '@phosphor-icons/react' import { useCallback, useRef, useState } from 'react' import Dropzone, { DropEvent, DropzoneRef, FileRejection } from 'react-dropzone' import { v4 as uuidv4 } from 'uuid' import { uploadFileRequestMutation } from '../../lib/networking/mutations/uploadFileMutation' import { uploadImportFileRequestMutation, UploadImportFileType, } from '../../lib/networking/mutations/uploadImportFileMutation' import { showErrorToast } from '../../lib/toastHelpers' import { validateCsvFile } from '../../utils/csvValidator' import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' import { ModalContent, ModalOverlay, ModalRoot, ModalTitleBar, } from '../elements/ModalPrimitives' import { theme } from '../tokens/stitches.config' 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 contentType: string message?: string } type UploadInfo = { uploadSignedUrl?: string requestId?: string message?: string } 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: React.MouseEvent) => { if (dropzoneRef.current) { dropzoneRef.current.open() } event?.preventDefault() }, [dropzoneRef] ) const uploadSignedUrlForFile = async ( file: UploadingFile ): Promise => { let { contentType } = file if ( contentType == 'application/vnd.ms-excel' && file.name.endsWith('.csv') ) { contentType = 'text/csv' } switch (contentType) { case 'text/csv': { let urlCount = 0 try { const csvData = await validateCsvFile(file.file) urlCount = csvData.data.length if (urlCount > 5000) { return { message: 'Due to an increase in traffic we are limiting CSV imports to 5000 items.', } } if (csvData.inValidData.length > 0) { return { message: csvData.inValidData[0].message, } } if (urlCount === 0) { return { message: 'No URLs found in CSV file.', } } } catch (error) { return { message: 'Invalid CSV file.', } } try { const result = await uploadImportFileRequestMutation( UploadImportFileType.URL_LIST, contentType ) return { uploadSignedUrl: result?.uploadSignedUrl, message: `Importing ${urlCount} URLs`, } } catch (error) { console.log('caught error', error) if (error == 'UPLOAD_DAILY_LIMIT_EXCEEDED') { return { message: 'You have exceeded your maximum daily upload limit.', } } } } case 'application/zip': { const result = await uploadImportFileRequestMutation( UploadImportFileType.MATTER, contentType ) return { uploadSignedUrl: result?.uploadSignedUrl, } } case 'application/pdf': case 'application/epub+zip': { 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: contentType, createPageEntry: true, }) return { uploadSignedUrl: request?.uploadSignedUrl, requestId: request?.createdPageId, } } } return { message: `Invalid content type: ${contentType}`, } } const handleAcceptedFiles = useCallback( (acceptedFiles: any, event: DropEvent) => { setInDragOperation(false) const addedFiles = acceptedFiles.map( (file: { name: any; type: string }) => { return { id: uuidv4(), file: file, name: file.name, progress: 0, status: 'inprogress', contentType: file.type, } } ) const allFiles = [...uploadFiles, ...addedFiles] setUploadFiles(allFiles) ;(async () => { for (const file of addedFiles) { try { const uploadInfo = await uploadSignedUrlForFile(file) if (!uploadInfo.uploadSignedUrl) { const message = uploadInfo.message || 'No upload URL available' showErrorToast(message, { duration: 10000 }) file.status = 'error' setUploadFiles([...allFiles]) return } const uploadResult = await axios.request({ method: 'PUT', url: uploadInfo.uploadSignedUrl, data: file.file, withCredentials: false, headers: { 'Content-Type': file.file.type, }, onUploadProgress: (p) => { if (!p.total) { console.warn('No total available for upload progress') return } const progress = (p.loaded / p.total) * 100 file.progress = progress setUploadFiles([...allFiles]) }, }) file.progress = 100 file.status = 'success' file.openUrl = uploadInfo.requestId ? `/article/sr/${uploadInfo.requestId}` : undefined file.message = uploadInfo.message setUploadFiles([...allFiles]) } catch (error) { file.status = 'error' setUploadFiles([...allFiles]) } } })() }, [uploadFiles] ) return ( { event.preventDefault() }} > The uploader is currently disabled. {/* { 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={{ 'text/csv': ['.csv'], 'application/zip': ['.zip'], '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) => { return ( {file.name} {file.status != 'inprogress' ? ( {file.status == 'success' && file.openUrl && ( Read Now )} {file.status == 'success' && !file.openUrl && ( {file.message || 'Your import has started'} )} {file.status == 'error' && ( Error Uploading )} ) : ( {' '} )} ) })}
)}
*/}
) }