Let's make a React component for uploading a file to a distributed storage IPFS.
import { InputWrapperWithErrorMessage } from "lib/ui/inputs/InputWrapper"
import { useEffect } from "react"
import { FileInput } from "lib/ui/inputs/FileInput"
import { SelectedFile } from "lib/ui/inputs/SelectedFile"
import { useUploadFileMutation } from "web3/hooks/useUploadFileMutation"
import { getDistributedFileName } from "web3/utils/getDistributedFileName"
interface Props {
value?: string
error?: string
onChange: (value?: string) => void
}
export const PdfFileInput = ({ value, error, onChange }: Props) => {
const {
mutate: uploadFile,
data: uploadedFileUri,
reset,
isLoading,
} = useUploadFileMutation()
useEffect(() => {
if (uploadedFileUri && !value) {
onChange(uploadedFileUri)
reset()
}
}, [onChange, reset, uploadedFileUri, value])
return (
<InputWrapperWithErrorMessage label="Upload a file" error={error}>
{value ? (
<SelectedFile
name={getDistributedFileName(value)}
onRemove={() => onChange(undefined)}
/>
) : (
<FileInput
isLoading={isLoading}
onSubmit={(file) => {
uploadFile(file)
}}
accept={{
"application/pdf": [".pdf"],
}}
/>
)}
</InputWrapperWithErrorMessage>
)
}
Here we have a PDFFileInput
component with value
, error
, and onChange
properties. First, we render a wrapper that displays the label and error, if any. Then based on value presence, we show either the SelectedFile
component or the FileInput
.
import styled from "styled-components"
import { Card } from "lib/ui/Card"
import { inputBackgroundCSS, inputBorderRadiusCSS } from "./config"
import { useDropzone, Accept } from "react-dropzone"
import { VStack } from "lib/ui/Stack"
import { Text } from "lib/ui/Text"
import { defaultTransitionCSS } from "lib/ui/animations/transitions"
import { UploadIcon } from "lib/ui/icons/UploadIcon"
import { OutlinedButton } from "lib/ui/buttons/rect/OutlinedButton"
interface Props {
onSubmit: (file: File) => void
accept: Accept
isLoading?: boolean
}
const Container = styled(Card)`
flex: 1;
padding: 32px;
${inputBorderRadiusCSS};
${inputBackgroundCSS};
cursor: pointer;
${defaultTransitionCSS};
&:hover {
background: ${({ theme }) => theme.colors.backgroundGlass2.toCssValue()};
}
`
export const FileInput = ({ onSubmit, accept, isLoading }: Props) => {
const { getRootProps, getInputProps } = useDropzone({
maxFiles: 1,
accept,
onDrop: (files) => {
onSubmit(files[0])
},
})
const acceptedExtensions = Object.values(accept).flat()
return (
<Container {...getRootProps()}>
<VStack gap={16} alignItems="center">
<Text size={34} color="supporting">
<UploadIcon />
</Text>
<Text color="supporting">
Drag and drop a{" "}
{acceptedExtensions
.map((extension) => extension.toUpperCase())
.join("/")}{" "}
file here or
</Text>
<OutlinedButton isLoading={isLoading} as="div">
Click to upload
</OutlinedButton>
</VStack>
<input {...getInputProps()} />
</Container>
)
}
The FileInput
component receives the onSubmit
, accept
, and isLoading
properties. We pass the accept object to the useDropzone
hook to allow only specific file types and the onDrop callback that will trigger the onSubmit
property. To allow dropping files into area input, we leverage the use dropzone hook from the react-dropzone
library and pass the getRootProps to the container and getInputProps to an invisible input element.
Once the user has selected a file, we use the useUploadFileMutation hook. Here we leverage the web3storage library to upload files to IPFS. The put method will return a content identifier from IPFS that we can wrap with the gateway prefix to get an accessible URL.
The problem with this hook implementation is exposing the web3storage key with our front-end code. So take this part and use it on the back end. One solution could be to upload the file to S3 using a pre-signed URL and then ask the back-end to duplicate that file to IPFS. Another thing to note is that IPFS might delete your data to make room for other content. To prevent that, you need to use a pinning service. There are plenty of them, but your would need to pay after exceeding the free tier.
After upload success, we show the SelectedFile
component with the files' name we extract with a tiny getDistributedFileName
helper. To reset the field and upload the file again, the user can press the "Remove" button.