Files
eventspace/frontend/src/hooks/usePresignedUpload.ts
Felipe Cardoso aaa7c68c12 Refactor file upload to use generated upload mutation
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.
2025-03-14 01:17:13 +01:00

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,
};
}