import type { Doc } from "@fileone/convex/_generated/dataModel" import { useMutation } from "@tanstack/react-query" import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai" import { atomEffect } from "jotai-effect" import { FilePlus2Icon, UploadCloudIcon, XIcon } from "lucide-react" import { nanoid } from "nanoid" import type React from "react" import { useId, useMemo, useRef, useState } from "react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Progress } from "@/components/ui/progress" import { WithAtom } from "@/components/with-atom" import { clearFileUploadAtom, fileUploadAtomFamily } from "./store" import useUploadFile from "./use-upload-file" type UploadFileDialogProps = { targetDirectory: Doc<"directories"> onClose: () => void } // Upload file atoms export type PickedFile = { id: string file: File } export const pickedFilesAtom = atom([]) export function UploadFileDialog({ targetDirectory, onClose, }: UploadFileDialogProps) { const formId = useId() const fileInputRef = useRef(null) const setPickedFiles = useSetAtom(pickedFilesAtom) const clearFileUpload = useSetAtom(clearFileUploadAtom) const store = useStore() const updateFileInputEffect = useMemo( () => atomEffect((get) => { const dataTransfer = new DataTransfer() const pickedFiles = get(pickedFilesAtom) for (const { file } of pickedFiles) { dataTransfer.items.add(file) } if (fileInputRef.current) { fileInputRef.current.files = dataTransfer.files } }), [], ) useAtom(updateFileInputEffect) const uploadFile = useUploadFile({ targetDirectory, }) const { mutate: uploadFiles, isPending: isUploading } = useMutation({ mutationFn: async (files: PickedFile[]) => { const promises = files.map((pickedFile) => uploadFile({ file: pickedFile.file, onStart: () => { store.set(fileUploadAtomFamily(pickedFile.id), 0) }, onProgress: (progress) => { store.set(fileUploadAtomFamily(pickedFile.id), progress) }, }).then(() => { clearFileUpload(pickedFile.id) }), ) await Promise.all(promises) }, onSuccess: () => { toast.success("All files uploaded successfully") setPickedFiles([]) onClose() }, }) function handleSubmit(event: React.FormEvent) { event.preventDefault() } function openFilePicker() { fileInputRef.current?.click() } function handleFileChange(event: React.ChangeEvent) { const files = event.target.files if (files) { setPickedFiles((prev) => [ ...prev, ...Array.from(files).map((file) => ({ id: nanoid(), file })), ]) } } function startFileUpload() { const pickedFiles = store.get(pickedFilesAtom) uploadFiles(pickedFiles) } let dialogTitle: string let dialogDescription: string if (isUploading) { dialogTitle = "Uploading files" dialogDescription = "You can close the dialog while they are being uploaded in the background." } else if (targetDirectory.name) { dialogTitle = `Upload file to "${targetDirectory.name}"` dialogDescription = "Drag and drop files here or click to select files" } else { dialogTitle = "Upload file" dialogDescription = "Drag and drop files here or click to select files" } return ( { if (!open) onClose() }} > {dialogTitle} {dialogDescription}
{(pickedFiles) => ( <> {pickedFiles.length > 0 ? ( ) : null} )}
) } function UploadFileDropContainer({ children }: React.PropsWithChildren) { const [draggedFiles, setDraggedFiles] = useState([]) const setPickedFiles = useSetAtom(pickedFilesAtom) function handleDragOver(e: React.DragEvent) { e.preventDefault() const items = Array.from(e.dataTransfer.items) const draggedFiles = [] for (const item of items) { if (item.kind === "file") { draggedFiles.push(item) } } setDraggedFiles(draggedFiles) } function handleDragLeave() { setDraggedFiles([]) } function handleDrop(e: React.DragEvent) { e.preventDefault() const items = Array.from(e.dataTransfer.items) const droppedFiles: PickedFile[] = [] for (const item of items) { const file = item.getAsFile() if (file) { droppedFiles.push({ id: nanoid(), file, }) } } setPickedFiles((prev) => [...prev, ...droppedFiles]) setDraggedFiles([]) } return (
{children} {draggedFiles.length > 0 ? (

Drop {draggedFiles.length} files here

) : null}
) } function UploadFileArea({ onClick }: { onClick: () => void }) { const [pickedFiles, setPickedFiles] = useAtom(pickedFilesAtom) function removeSelectedFile(file: PickedFile) { setPickedFiles((prev) => prev.filter((f) => f.id !== file.id)) } if (pickedFiles.length > 0) { return ( ) } return ( ) } function PickedFilesList({ pickedFiles, onRemoveFile, }: { pickedFiles: PickedFile[] onRemoveFile: (file: PickedFile) => void }) { return (
    {pickedFiles.map((file: PickedFile) => ( ))}
) } function PickedFileItem({ file: pickedFile, onRemove, }: { file: PickedFile onRemove: (file: PickedFile) => void }) { const fileUploadAtom = fileUploadAtomFamily(pickedFile.id) const fileUpload = useAtomValue(fileUploadAtom) console.log("fileUpload", fileUpload) const { file, id } = pickedFile return (
  • {file.name} {fileUpload ? ( ) : ( )}
  • ) }