import { useRef, useState, useReducer, Reducer, useEffect, useCallback, } from "react"; import { attributes } from "../utils/attributes"; import { PrettyBytes } from "../utils/bytes"; import { Toast } from "../Toast/Toast"; import { UploadStatus } from "./types"; import { CircleCheck, TriangleAlert, CircleX, CircleStop } from "lucide-react"; import { Spinner } from "../Spinner/Spinner"; import { CodexData } from "@codex-storage/sdk-js"; import { WebFileIcon } from "../WebFileIcon/WebFileIcon"; import { ButtonIcon } from "../ButtonIcon/ButtonIcon"; import { SimpleText } from "../SimpleText/SimpleText"; type UploadFileProps = { file: File; onClose: (id: string) => void; id: string; onSuccess: ((cid: string, file: File) => void) | undefined; codexData: CodexData; // useWorker: boolean; }; type State = { progress: { loaded: number; total: number }; cid: string; preview: string; status: UploadStatus; error: string; }; type Action = | { type: "reset"; } | { type: "progress"; loaded: number; total: number; } | { type: "preview"; preview: string; } | { type: "completed"; cid: string; } | { type: "cancel"; } | { type: "delete"; } | { type: "error"; error: string; }; function reducer(state: State, action: Action) { switch (action.type) { case "progress": { const { loaded, total } = action; return { ...state, progress: { loaded, total }, status: loaded === total ? "done" : state.status, }; } case "preview": { return { ...state, preview: action.preview, }; } case "completed": { return { ...state, // Just to ensure the file upload is in done status, // in case of the onprogress callback function was not called status: "done" as UploadStatus, cid: action.cid, }; } case "cancel": { return { ...state, status: "error" as UploadStatus, error: "The upload has been cancelled.", }; } case "delete": { return { progress: { loaded: 0, total: 0 }, cid: "", preview: "", status: "progress" as UploadStatus, error: "", }; } case "error": { return { ...state, error: action.error, status: "error" as UploadStatus }; } default: { return state; } } } const isImage = (type: string) => type.startsWith("image"); export function UploadFile({ file, onClose, id, onSuccess, codexData, // useWorker, }: UploadFileProps) { const abort = useRef<(() => void) | null>(null); // const worker = useRef(null); const [toast, setToast] = useState({ time: 0, message: "" }); const [state, dispatch] = useReducer>(reducer, { progress: { loaded: 0, total: 0 }, cid: "", preview: "", status: "progress" as UploadStatus, error: "", }); const upload = useCallback(async () => { const { abort: a, result } = codexData.upload(file, onProgress); abort.current = a; const res = await result; if (res.error) { dispatch({ type: "error", error: res.data.message }); return; } dispatch({ type: "completed", cid: res.data }); onSuccess?.(res.data, file); }, [codexData, onSuccess, file]); const init = useRef(false); const onProgress = (loaded: number, total: number) => { dispatch({ type: "progress", loaded, total, }); }; useEffect(() => { if (init.current) { return; } init.current = true; if (isImage(file.type)) { const reader = new FileReader(); reader.onload = () => { const preview = reader.result?.toString(); if (preview) { dispatch({ type: "preview", preview }); } }; reader.readAsDataURL(file); } upload(); // if (useWorker) { // worker.current = new Worker(new URL("./worker", import.meta.url), { // type: "module", // }); // provider().then(() => { // worker.current?.postMessage({ type: "init", upload: "" }); // }); // worker.current.onmessage = function (e) { // const data = e.data; // if (e.data.type === "progress") { // onProgress(data.loaded, data.total); // } else if (e.data.type === "completed") { // onInternalSuccess(e.data.value.data); // } else if (e.data.error) { // // TODO report with sentry // dispatch({ type: "error", error: e.data.error }); // } // }; // worker.current.onerror = function (e) { // // TODO report to sentry // console.error("Error in worker:", e); // dispatch({ type: "error", error: e.message }); // worker.current?.terminate(); // }; // worker.current.postMessage({ type: "file", file }); // } else { // mutateAsync(file); // } }, [file, upload, codexData]); const onCancel = () => { // if (worker.current) { // worker.current.postMessage({ type: "abort" }); // } else { // abort.current?.(); // } abort.current?.(); const type = state.status === "progress" ? "cancel" : "delete"; dispatch({ type }); }; const onInternalClose = () => { // if (worker.current) { // worker.current.postMessage({ type: "abort" }); // } else { // abort.current?.(); // } abort.current?.(); onClose(id); }; const onCopy = () => { if (cid) { navigator.clipboard.writeText(cid); setToast({ time: Date.now(), message: "The CID has been copied to your clipboard.", }); } }; const parts = file.name.split("."); const extension = parts.pop(); const filename = parts.join("."); const { cid, error = "", preview, progress, status } = state; const onAction = state.status === "progress" ? onCancel : onInternalClose; const percent = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0; const ActionIcon = () => ; return (
{preview ? ( Preview ) : ( )}
{filename} .{extension}
{PrettyBytes(file.size)}
{percent.toFixed(2)} %
{!!cid && ( <>
Success ! Click on the CID to copy it to your clipboard.
{cid} )} {error ? error : <> }
); } type UploadStatusIconProps = { status: UploadStatus; }; export function UploadStatusIcon({ status }: UploadStatusIconProps) { switch (status) { case "done": return ( ); case "error": return ( ); case "progress": return ; } } function UploadActionIcon({ status }: UploadStatusIconProps) { switch (status) { case "error": case "done": return ; case "progress": return ; } }