| "use client"; |
|
|
| import { createClient } from "@midday/supabase/client"; |
| import { cn } from "@midday/ui/cn"; |
| import { useToast } from "@midday/ui/use-toast"; |
| import { useMutation } from "@tanstack/react-query"; |
| import { type ReactNode, useEffect, useRef, useState } from "react"; |
| import { type FileRejection, useDropzone } from "react-dropzone"; |
| import { useUserQuery } from "@/hooks/use-user"; |
| import { useTRPC } from "@/trpc/client"; |
| import { resumableUpload } from "@/utils/upload"; |
|
|
| type UploadResult = { |
| filename: string; |
| file: File; |
| }; |
|
|
| type Props = { |
| children: ReactNode; |
| onUpload?: ( |
| results: { |
| file_path: string[]; |
| mimetype: string; |
| size: number; |
| }[], |
| ) => void; |
| }; |
|
|
| export function VaultUploadZone({ onUpload, children }: Props) { |
| const trpc = useTRPC(); |
| const { data: user } = useUserQuery(); |
| const supabase = createClient(); |
| const [progress, setProgress] = useState(0); |
| const [showProgress, setShowProgress] = useState(false); |
| const [toastId, setToastId] = useState<string | null>(null); |
| const uploadProgress = useRef<number[]>([]); |
| const { toast, dismiss, update } = useToast(); |
|
|
| const processDocumentMutation = useMutation( |
| trpc.documents.processDocument.mutationOptions(), |
| ); |
|
|
| useEffect(() => { |
| if (!toastId && showProgress) { |
| const { id } = toast({ |
| title: `Uploading ${uploadProgress.current.length} files`, |
| progress, |
| variant: "progress", |
| description: "Please do not close browser until completed", |
| duration: Number.POSITIVE_INFINITY, |
| }); |
|
|
| setToastId(id); |
| } else if (toastId) { |
| update(toastId, { |
| id: toastId, |
| title: `Uploading ${uploadProgress.current.length} files`, |
| progress, |
| variant: "progress", |
| }); |
| } |
| }, [showProgress, progress, toastId]); |
|
|
| const onDrop = async (files: File[]) => { |
| |
| if (!files.length) { |
| return; |
| } |
|
|
| |
| uploadProgress.current = files.map(() => 0); |
|
|
| setShowProgress(true); |
|
|
| |
| const path = [user?.teamId] as string[]; |
|
|
| try { |
| const results = (await Promise.all( |
| files.map(async (file: File, idx: number) => |
| resumableUpload(supabase, { |
| bucket: "vault", |
| path, |
| file, |
| onProgress: (bytesUploaded: number, bytesTotal: number) => { |
| uploadProgress.current[idx] = (bytesUploaded / bytesTotal) * 100; |
|
|
| const _progress = uploadProgress.current.reduce( |
| (acc, currentValue) => { |
| return acc + currentValue; |
| }, |
| 0, |
| ); |
|
|
| setProgress(Math.round(_progress / files.length)); |
| }, |
| }), |
| ), |
| )) as UploadResult[]; |
|
|
| |
| processDocumentMutation.mutate( |
| results.map((result) => ({ |
| filePath: [...path, result.filename], |
| mimetype: result.file.type, |
| size: result.file.size, |
| })), |
| ); |
|
|
| |
| uploadProgress.current = []; |
|
|
| setProgress(0); |
| toast({ |
| title: "Upload successful.", |
| variant: "success", |
| duration: 2000, |
| }); |
|
|
| setShowProgress(false); |
| setToastId(null); |
| if (toastId) { |
| dismiss(toastId); |
| } |
|
|
| |
| const typedResults = results.map((result) => ({ |
| file_path: [...path, result.filename], |
| mimetype: result.file.type, |
| size: result.file.size, |
| })); |
|
|
| onUpload?.(typedResults); |
| } catch { |
| toast({ |
| duration: 2500, |
| variant: "error", |
| title: "Something went wrong please try again.", |
| }); |
| } |
| }; |
|
|
| const { getRootProps, getInputProps, isDragActive } = useDropzone({ |
| onDrop, |
| onDropRejected: ([reject]: FileRejection[]) => { |
| if (reject?.errors.find(({ code }) => code === "file-too-large")) { |
| toast({ |
| duration: 2500, |
| variant: "error", |
| title: "File size to large.", |
| }); |
| } |
|
|
| if (reject?.errors.find(({ code }) => code === "file-invalid-type")) { |
| toast({ |
| duration: 2500, |
| variant: "error", |
| title: "File type not supported.", |
| }); |
| } |
| }, |
| maxSize: 5000000, |
| maxFiles: 25, |
| accept: { |
| "image/*": [".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".avif"], |
| "application/pdf": [".pdf"], |
| "application/msword": [".doc"], |
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document": |
| [".docx"], |
| "application/vnd.oasis.opendocument.text": [".odt"], |
| "application/vnd.ms-excel": [".xls"], |
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ |
| ".xlsx", |
| ], |
| "application/vnd.oasis.opendocument.spreadsheet": [".ods"], |
| "application/vnd.ms-powerpoint": [".ppt"], |
| "application/vnd.openxmlformats-officedocument.presentationml.presentation": |
| [".pptx"], |
| "application/vnd.oasis.opendocument.presentation": [".odp"], |
| "text/plain": [".txt"], |
| "text/csv": [".csv"], |
| "text/markdown": [".md"], |
| "application/rtf": [".rtf"], |
| "application/zip": [".zip"], |
| }, |
| }); |
|
|
| return ( |
| <div |
| className="relative h-full" |
| {...getRootProps({ onClick: (evt) => evt.stopPropagation() })} |
| > |
| <div className="absolute top-0 right-0 left-0 z-[51] w-full pointer-events-none h-[calc(100vh-150px)]"> |
| <div |
| className={cn( |
| "bg-background h-full w-full flex items-center justify-center text-center", |
| isDragActive ? "visible" : "invisible", |
| )} |
| > |
| <input {...getInputProps()} id="upload-files" /> |
| |
| <div className="flex flex-col items-center justify-center gap-2"> |
| <p className="text-xs"> |
| Drop your documents and files here. <br /> |
| Maximum of 25 files at a time. |
| </p> |
| |
| <span className="text-xs text-[#878787]">Max file size 5MB</span> |
| </div> |
| </div> |
| </div> |
| |
| {children} |
| </div> |
| ); |
| } |
|
|