Add components and hooks for event theme management
Introduced the `usePresignedUpload` hook for file uploads and multiple components for event theme management, including form handling, asset uploads, and UI enhancements. These additions support creating, editing, and managing event themes effectively.
This commit is contained in:
186
frontend/src/hooks/usePresignedUpload.ts
Normal file
186
frontend/src/hooks/usePresignedUpload.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { generatePresignedUrl, PresignedUrlResponse } from "@/client";
|
||||
|
||||
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);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const uploadFileToPresignedUrl = useCallback(
|
||||
async (file: File, uploadUrl: string): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.onprogress = ({ loaded, total }) => {
|
||||
const progress = total ? Math.round((loaded / total) * 100) : 0;
|
||||
setUploadState((prev) => ({ ...prev, progress }));
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () =>
|
||||
reject(new Error("Network error during file upload."));
|
||||
xhr.open("PUT", uploadUrl);
|
||||
xhr.setRequestHeader("Content-Type", file.type);
|
||||
xhr.send(file);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
},
|
||||
[reset, fileType, onUploadError],
|
||||
);
|
||||
|
||||
const uploadFile = useCallback(async () => {
|
||||
if (!uploadState.file) {
|
||||
const error = "File not selected.";
|
||||
setUploadState((prev) => ({ ...prev, error }));
|
||||
onUploadError?.(error);
|
||||
return;
|
||||
}
|
||||
|
||||
setUploadState((prev) => ({
|
||||
...prev,
|
||||
isUploading: true,
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
}));
|
||||
|
||||
try {
|
||||
const { upload_url, file_url } = await presignedUrlMutation.mutateAsync(
|
||||
uploadState.file,
|
||||
);
|
||||
|
||||
await uploadFileToPresignedUrl(uploadState.file, upload_url);
|
||||
|
||||
setUploadState((prev) => ({
|
||||
...prev,
|
||||
isUploading: false,
|
||||
fileUrl: file_url,
|
||||
progress: 100,
|
||||
}));
|
||||
|
||||
onUploadSuccess?.(file_url);
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unexpected error during upload.";
|
||||
setUploadState((prev) => ({
|
||||
...prev,
|
||||
isUploading: false,
|
||||
error: message,
|
||||
}));
|
||||
onUploadError?.(message);
|
||||
}
|
||||
}, [
|
||||
uploadState.file,
|
||||
presignedUrlMutation,
|
||||
uploadFileToPresignedUrl,
|
||||
onUploadSuccess,
|
||||
onUploadError,
|
||||
]);
|
||||
|
||||
return {
|
||||
...uploadState,
|
||||
selectFile,
|
||||
uploadFile,
|
||||
reset,
|
||||
isPresignedUrlLoading: presignedUrlMutation.isPending,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user