Show uploaded items in the upload modal

This commit is contained in:
Jackson Harper
2023-04-13 19:00:16 +08:00
parent 7d7fdcd2e3
commit 12163de073
6 changed files with 469 additions and 237 deletions

View File

@ -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',

View File

@ -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<UploadingFile[]>([
// {
// id: uuidv4(),
// file: '',
// name: 'test file',
// status: 'inprogress',
// progress: (371712 / 864476) * 100,
// openUrl: '',
// },
])
const [inDragOperation, setInDragOperation] = useState(false)
const dropzoneRef = useRef<DropzoneRef | null>(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 (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
css={{
bg: '$grayBg',
px: '24px',
minWidth: '650px',
minHeight: '430px',
}}
onInteractOutside={() => {
// remove focus from modal
;(document.activeElement as HTMLElement).blur()
}}
>
<VStack distribution="start">
<ModalTitleBar
title="Upload File"
onOpenChange={props.onOpenChange}
/>
<Dropzone
ref={dropzoneRef}
onDragEnter={() => {
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,
}) => (
<div
{...getRootProps({ className: 'dropzone' })}
style={{ height: '100%', width: '100%' }}
>
<DragnDropContainer>
<DragnDropStyle>
<DragnDropIndicator
css={{
border: inDragOperation ? '2px dashed blue' : 'unset',
}}
>
<VStack alignment="center" css={{ gap: '10px' }}>
<File
size={48}
color={theme.colors.thTextSubtle2.toString()}
/>
{inDragOperation ? (
<>
<Box
css={{
fontWeight: '800',
fontSize: '20px',
}}
>
Drop to upload your file
</Box>
</>
) : (
<>
<Box
css={{
fontWeight: '800',
fontSize: '20px',
}}
>
Drag files here to add them to your library
</Box>
<Box
css={{
fontSize: '14px',
}}
>
Or{' '}
<a href="" onClick={openDialog}>
choose your files
</a>
</Box>
</>
)}
</VStack>
</DragnDropIndicator>
</DragnDropStyle>
<VStack css={{ width: '100%', mt: '25px', gap: '5px' }}>
{uploadFiles.map((file) => {
console.log('fileL ', file.name, file.progress)
return (
<HStack
key={file.id}
css={{
width: '100%',
height: '54px',
border: '1px solid $grayBorder',
borderRadius: '5px',
padding: '15px',
gap: '10px',
}}
alignment="center"
distribution="start"
>
<Box
css={{
width: '280px',
maxLines: '1',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '200px',
overflow: 'hidden',
fontSize: '14px',
fontWeight: 'bold',
}}
>
{file.name}
</Box>
{file.progress == 100 ? (
<HStack
alignment="center"
css={{ marginLeft: 'auto', fontSize: '14px' }}
>
{file.status == 'success' && (
<a href={file.openUrl}>Read Now</a>
)}
{file.status == 'error' && (
<SpanBox css={{ color: 'red' }}>
Error Uploading
</SpanBox>
)}
</HStack>
) : (
<ProgressRoot value={file.progress} max={100}>
<ProgressIndicator
style={{
transform: `translateX(-${
100 - file.progress
}%)`,
}}
/>{' '}
</ProgressRoot>
)}
</HStack>
)
})}
</VStack>
</DragnDropContainer>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
</VStack>
</ModalContent>
</ModalRoot>
)
}

View File

@ -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<HTMLDivElement>(null)
const [labelsTarget, setLabelsTarget] =
useState<LibraryItem | undefined>(undefined)
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
undefined
)
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
@ -611,33 +609,6 @@ type HomeFeedContentProps = {
) => Promise<void>
}
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<LayoutType>({
@ -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 (
<>
<VStack
@ -803,121 +725,53 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
<Toaster />
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
<Dropzone
onDrop={handleDrop}
onDragEnter={() => {
setInDragOperation(true)
}}
onDragLeave={() => {
setInDragOperation(false)
}}
preventDropOnDocument={true}
noClick={true}
accept={{
'application/pdf': ['.pdf'],
<div
onDragEnter={(event) => {
setShowUploadModal(true)
}}
style={{ height: '100%', width: '100%' }}
>
{({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
<div
{...getRootProps({ className: 'dropzone' })}
style={{ height: '100%', width: '100%' }}
>
{inDragOperation && uploadingFiles.length < 1 && (
<DragnDropContainer>
<DragnDropStyle>
<Box
css={{
color: '$utilityTextDefault',
fontWeight: '800',
fontSize: '$4',
}}
>
Drop PDF document to to upload and add to your library
</Box>
</DragnDropStyle>
</DragnDropContainer>
)}
{uploadingFiles.length > 0 && (
<DragnDropContainer>
<DragnDropStyle>
<Box
css={{
color: '$utilityTextDefault',
fontWeight: '800',
fontSize: '$4',
width: '80%',
}}
>
<Progress.Root
className="ProgressRoot"
value={uploadProgress}
>
<Progress.Indicator
className="ProgressIndicator"
style={{
transform: `translateX(-${100 - uploadProgress}%)`,
}}
/>
</Progress.Root>
<StyledText
style="boldText"
css={{
color: theme.colors.omnivoreGray.toString(),
}}
>
Uploading file
</StyledText>
</Box>
</DragnDropStyle>
</DragnDropContainer>
)}
<input {...getInputProps()} />
{!props.isValidating && props.items.length == 0 ? (
<EmptyLibrary
onAddLinkClicked={() => {
props.setShowAddLinkModal(true)
}}
/>
) : (
<LibraryItems
items={props.items}
layout={props.layout}
viewer={props.viewer}
gridContainerRef={props.gridContainerRef}
setShowEditTitleModal={props.setShowEditTitleModal}
setLinkToEdit={props.setLinkToEdit}
setShowUnsubscribeConfirmation={
setShowUnsubscribeConfirmation
}
setLinkToRemove={props.setLinkToRemove}
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation}
actionHandler={props.actionHandler}
/>
)}
<HStack
distribution="center"
css={{ width: '100%', mt: '$2', mb: '$4' }}
>
{props.hasMore ? (
<Button
style="ctaGray"
css={{
cursor: props.isValidating ? 'not-allowed' : 'pointer',
}}
onClick={props.loadMore}
disabled={props.isValidating}
>
{props.isValidating ? 'Loading' : 'Load More'}
</Button>
) : (
<StyledText style="caption"></StyledText>
)}
</HStack>
</div>
{!props.isValidating && props.items.length == 0 ? (
<EmptyLibrary
onAddLinkClicked={() => {
props.setShowAddLinkModal(true)
}}
/>
) : (
<LibraryItems
items={props.items}
layout={props.layout}
viewer={props.viewer}
gridContainerRef={props.gridContainerRef}
setShowEditTitleModal={props.setShowEditTitleModal}
setLinkToEdit={props.setLinkToEdit}
setShowUnsubscribeConfirmation={setShowUnsubscribeConfirmation}
setLinkToRemove={props.setLinkToRemove}
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation}
actionHandler={props.actionHandler}
/>
)}
</Dropzone>
<HStack
distribution="center"
css={{ width: '100%', mt: '$2', mb: '$4' }}
>
{props.hasMore ? (
<Button
style="ctaGray"
css={{
cursor: props.isValidating ? 'not-allowed' : 'pointer',
}}
onClick={props.loadMore}
disabled={props.isValidating}
>
{props.isValidating ? 'Loading' : 'Load More'}
</Button>
) : (
<StyledText style="caption"></StyledText>
)}
</HStack>
</div>
</VStack>
{props.showEditTitleModal && (
<EditLibraryItemModal
@ -995,6 +849,9 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
}}
/>
)}
{showUploadModal && (
<UploadModal onOpenChange={() => setShowUploadModal(false)} />
)}
</>
)
}

View File

@ -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"
>
<Button
css={{
height: '40px',
p: '15px',
pr: '20px',
fontSize: '14px',
verticalAlign: 'center',
<HStack css={{ gap: '15px' }}>
<Button
css={{
height: '40px',
p: '15px',
pr: '20px',
fontSize: '14px',
verticalAlign: '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()
}}
>
<Plus size={16} weight="bold" />
<SpanBox css={{ width: '10px' }}></SpanBox>Add Link
</Button>
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()
}}
>
<Plus size={16} weight="bold" />
<SpanBox css={{ width: '10px' }}></SpanBox>Add Link
</Button>
<Button
css={{
height: '40px',
p: '15px',
pr: '20px',
fontSize: '14px',
verticalAlign: '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()
}}
>
<Upload size={16} weight="bold" />
</Button>
</HStack>
</VStack>
</>
)

View File

@ -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
}
}
}`

View File

@ -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);
}