Add components and hooks for event theme management
Some checks failed
Build and Push Docker Images / changes (push) Successful in 5s
Build and Push Docker Images / build-backend (push) Has been skipped
Build and Push Docker Images / build-frontend (push) Failing after 49s

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:
2025-03-13 08:36:20 +01:00
parent f2f8d85775
commit 4044d85410
7 changed files with 1137 additions and 0 deletions

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