Replaced direct SDK file upload calls with generated React Query mutations for better progress tracking and error handling. Streamlined logic for obtaining presigned URLs and uploading files, ensuring consistency and maintainability. Removed redundant code and improved state management during the upload process.
210 lines
5.2 KiB
TypeScript
210 lines
5.2 KiB
TypeScript
import { useState, useCallback, useEffect } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { generatePresignedUrl, PresignedUrlResponse } from "@/client";
|
|
import { uploadFileMutation } from "@/client/@tanstack/react-query.gen";
|
|
|
|
export interface FileUploadState {
|
|
file?: File;
|
|
preview?: string;
|
|
isUploading: boolean;
|
|
progress: number;
|
|
error?: string;
|
|
fileUrl?: string;
|
|
}
|
|
|
|
const initialState: FileUploadState = {
|
|
file: undefined,
|
|
preview: undefined,
|
|
isUploading: false,
|
|
progress: 0,
|
|
error: undefined,
|
|
fileUrl: undefined,
|
|
};
|
|
|
|
export interface UsePresignedUploadOptions {
|
|
fileType?: "image" | "document";
|
|
purpose?: string;
|
|
onUploadSuccess?: (fileUrl: string) => void;
|
|
onUploadError?: (error: string) => void;
|
|
}
|
|
|
|
export function usePresignedUpload(options: UsePresignedUploadOptions = {}) {
|
|
const {
|
|
fileType = "image",
|
|
purpose = "general",
|
|
onUploadSuccess,
|
|
onUploadError,
|
|
} = options;
|
|
|
|
const [uploadState, setUploadState] = useState<FileUploadState>(initialState);
|
|
|
|
// Use the presigned URL mutation
|
|
const presignedUrlMutation = useMutation({
|
|
mutationFn: async (file: File): Promise<PresignedUrlResponse> => {
|
|
const { data } = await generatePresignedUrl({
|
|
body: {
|
|
filename: file.name,
|
|
content_type: file.type,
|
|
folder: purpose,
|
|
},
|
|
});
|
|
|
|
if (!data || !data.upload_url || !data.file_url) {
|
|
throw new Error("Invalid response obtaining presigned URLs");
|
|
}
|
|
console.log("Presigned URL response: ", data);
|
|
return data;
|
|
},
|
|
});
|
|
|
|
// Use the generated upload file mutation
|
|
const uploadMutation = useMutation(
|
|
uploadFileMutation({
|
|
onUploadProgress: (progressEvent: any) => {
|
|
const progress =
|
|
progressEvent.loaded && progressEvent.total
|
|
? Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
|
: 0;
|
|
setUploadState((prev) => ({ ...prev, progress }));
|
|
},
|
|
}),
|
|
);
|
|
|
|
const uploadFileToPresignedUrl = useCallback(
|
|
async (file: File, uploadUrl: string): Promise<void> => {
|
|
return new Promise<void>((resolve, reject) => {
|
|
try {
|
|
// Get the token from the upload URL
|
|
const token = uploadUrl.split("/").pop();
|
|
|
|
if (!token) {
|
|
reject(new Error("Invalid upload URL - missing token"));
|
|
return;
|
|
}
|
|
|
|
// Use the generated mutation instead of direct SDK call
|
|
uploadMutation.mutate(
|
|
{
|
|
path: { token },
|
|
body: { file },
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
resolve();
|
|
},
|
|
onError: (error: any) => {
|
|
reject(
|
|
new Error(
|
|
`Upload failed: ${error.message || "Unknown error"}`,
|
|
),
|
|
);
|
|
},
|
|
},
|
|
);
|
|
} catch (error) {
|
|
reject(new Error(`Network error during file upload: ${error}`));
|
|
}
|
|
});
|
|
},
|
|
[uploadMutation],
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (uploadState.preview) {
|
|
URL.revokeObjectURL(uploadState.preview);
|
|
}
|
|
};
|
|
}, [uploadState.preview]);
|
|
|
|
const reset = useCallback(() => {
|
|
setUploadState((prev) => {
|
|
if (prev.preview) URL.revokeObjectURL(prev.preview);
|
|
return initialState;
|
|
});
|
|
}, []);
|
|
|
|
const selectFile = useCallback(
|
|
(file: File | null) => {
|
|
reset();
|
|
|
|
if (!file) return;
|
|
|
|
if (fileType === "image" && !file.type.startsWith("image/")) {
|
|
const error = "Selected file must be an image.";
|
|
setUploadState((prev) => ({ ...prev, error }));
|
|
onUploadError?.(error);
|
|
return;
|
|
}
|
|
|
|
const preview =
|
|
fileType === "image" ? URL.createObjectURL(file) : undefined;
|
|
|
|
setUploadState({
|
|
file,
|
|
preview,
|
|
isUploading: false,
|
|
progress: 0,
|
|
error: undefined,
|
|
fileUrl: undefined,
|
|
});
|
|
},
|
|
[fileType, onUploadError, reset],
|
|
);
|
|
|
|
const uploadFile = useCallback(async () => {
|
|
const { file } = uploadState;
|
|
if (!file) return;
|
|
|
|
try {
|
|
setUploadState((prev) => ({
|
|
...prev,
|
|
isUploading: true,
|
|
error: undefined,
|
|
}));
|
|
|
|
// First get the presigned URL
|
|
const presignedData = await presignedUrlMutation.mutateAsync(file);
|
|
|
|
// Then upload to that URL
|
|
await uploadFileToPresignedUrl(file, presignedData.upload_url);
|
|
|
|
// Use the file_url directly from the presigned URL response
|
|
const fileUrl = presignedData.file_url;
|
|
|
|
setUploadState((prev) => ({
|
|
...prev,
|
|
isUploading: false,
|
|
progress: 100,
|
|
fileUrl,
|
|
}));
|
|
|
|
onUploadSuccess?.(fileUrl);
|
|
} catch (error: any) {
|
|
const errorMessage = error.message || "Unknown error during file upload";
|
|
|
|
setUploadState((prev) => ({
|
|
...prev,
|
|
isUploading: false,
|
|
progress: 0,
|
|
error: errorMessage,
|
|
}));
|
|
|
|
onUploadError?.(errorMessage);
|
|
}
|
|
}, [
|
|
uploadState.file,
|
|
presignedUrlMutation,
|
|
uploadFileToPresignedUrl,
|
|
onUploadSuccess,
|
|
onUploadError,
|
|
]);
|
|
|
|
return {
|
|
...uploadState,
|
|
selectFile,
|
|
uploadFile,
|
|
reset,
|
|
};
|
|
}
|